// C o p y r i g h t © 2 0 2 2 R a n g e p r o o f P t y L t d . A l l r i g h t s r e s e r v e d .
import UIKit
import Combine
import GRDB
import DifferenceKit
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
import SignalUtilitiesKit
// MARK: - L o g . C a t e g o r y
private extension Log . Category {
static let cat : Log . Category = . create ( " SessionTableViewController " , defaultLevel : . info )
}
// MARK: - S e s s i o n V i e w M o d e l A c c e s s i b l e
protocol SessionViewModelAccessible {
var viewModelType : AnyObject . Type { get }
}
// MARK: - S e s s i o n T a b l e V i e w C o n t r o l l e r
class SessionTableViewController < ViewModel > : BaseVC , UITableViewDataSource , UITableViewDelegate , SessionViewModelAccessible where ViewModel : ( SessionTableViewModel & ObservableTableSource ) {
typealias Section = ViewModel . Section
typealias TableItem = ViewModel . TableItem
typealias SectionModel = ViewModel . SectionModel
private let viewModel : ViewModel
private var hasLoadedInitialTableData : Bool = false
private var isLoadingMore : Bool = false
private var isAutoLoadingNextPage : Bool = false
private var viewHasAppeared : Bool = false
private var dataStreamJustFailed : Bool = false
private var dataChangeCancellable : AnyCancellable ?
private var disposables : Set < AnyCancellable > = Set ( )
private var onFooterTap : ( ( ) -> ( ) ) ?
public var viewModelType : AnyObject . Type { return type ( of : viewModel ) }
// MARK: - C o m p o n e n t s
private lazy var titleView : SessionTableViewTitleView = SessionTableViewTitleView ( )
private lazy var contentStackView : UIStackView = {
let result : UIStackView = UIStackView ( arrangedSubviews : [
infoBanner ,
tableView
] )
result . axis = . vertical
result . alignment = . fill
result . distribution = . fill
return result
} ( )
private lazy var infoBanner : InfoBanner = {
let result : InfoBanner = InfoBanner ( info : . empty )
result . isHidden = true
return result
} ( )
private lazy var tableView : UITableView = {
let result : UITableView = UITableView ( )
result . translatesAutoresizingMaskIntoConstraints = false
result . separatorStyle = . none
result . themeBackgroundColor = . clear
result . showsVerticalScrollIndicator = false
result . showsHorizontalScrollIndicator = false
result . register ( view : SessionCell . self )
result . register ( view : FullConversationCell . self )
result . registerHeaderFooterView ( view : SessionHeaderView . self )
result . registerHeaderFooterView ( view : SessionFooterView . self )
result . dataSource = self
result . delegate = self
result . sectionHeaderTopPadding = 0
return result
} ( )
private lazy var initialLoadLabel : UILabel = {
let result : UILabel = UILabel ( )
result . translatesAutoresizingMaskIntoConstraints = false
result . isUserInteractionEnabled = false
result . font = . systemFont ( ofSize : Values . smallFontSize )
result . themeTextColor = . textSecondary
result . text = viewModel . initialLoadMessage
result . textAlignment = . center
result . numberOfLines = 0
result . isHidden = ( viewModel . initialLoadMessage = = nil )
return result
} ( )
private lazy var emptyStateLabel : UILabel = {
let result : UILabel = UILabel ( )
result . translatesAutoresizingMaskIntoConstraints = false
result . isUserInteractionEnabled = false
result . font = . systemFont ( ofSize : Values . smallFontSize )
result . themeTextColor = . textSecondary
result . textAlignment = . center
result . numberOfLines = 0
result . isHidden = true
return result
} ( )
private lazy var fadeView : GradientView = {
let result : GradientView = GradientView ( )
result . themeBackgroundGradient = [
. value ( . backgroundPrimary , alpha : 0 ) , // W a n t t h i s t o t a k e u p 2 0 % ( ~ 2 5 p t )
. backgroundPrimary ,
. backgroundPrimary ,
. backgroundPrimary ,
. backgroundPrimary
]
result . set ( . height , to : Values . footerGradientHeight ( window : UIApplication . shared . keyWindow ) )
result . isHidden = true
return result
} ( )
private lazy var footerButton : SessionButton = {
let result : SessionButton = SessionButton ( style : . bordered , size : . medium )
result . translatesAutoresizingMaskIntoConstraints = false
result . addTarget ( self , action : #selector ( footerButtonTapped ) , for : . touchUpInside )
result . isHidden = true
return result
} ( )
// MARK: - I n i t i a l i z a t i o n
init ( viewModel : ViewModel ) {
self . viewModel = viewModel
( viewModel as ? ( any PagedObservationSource ) ) ? . didInit ( using : viewModel . dependencies )
super . init ( nibName : nil , bundle : nil )
}
required init ? ( coder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
deinit {
NotificationCenter . default . removeObserver ( self )
}
// MARK: - L i f e c y c l e
override func viewDidLoad ( ) {
super . viewDidLoad ( )
navigationItem . titleView = titleView
titleView . update ( title : self . viewModel . title , subtitle : self . viewModel . subtitle )
view . themeBackgroundColor = . backgroundPrimary
view . addSubview ( contentStackView )
view . addSubview ( initialLoadLabel )
view . addSubview ( emptyStateLabel )
view . addSubview ( fadeView )
view . addSubview ( footerButton )
setupLayout ( )
setupBinding ( )
// N o t i f i c a t i o n s
NotificationCenter . default . addObserver (
self ,
selector : #selector ( applicationDidBecomeActive ( _ : ) ) ,
name : UIApplication . didBecomeActiveNotification ,
object : nil
)
NotificationCenter . default . addObserver (
self ,
selector : #selector ( applicationDidResignActive ( _ : ) ) ,
name : UIApplication . didEnterBackgroundNotification , object : nil
)
}
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
startObservingChanges ( )
}
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( animated )
viewHasAppeared = true
autoLoadNextPageIfNeeded ( )
}
override func viewWillDisappear ( _ animated : Bool ) {
super . viewWillDisappear ( animated )
stopObservingChanges ( )
}
@objc func applicationDidBecomeActive ( _ notification : Notification ) {
// / N e e d t o d i s p a t c h t o t h e n e x t r u n l o o p t o p r e v e n t a p o s s i b l e c r a s h c a u s e d b y t h e d a t a b a s e r e s u m i n g m i d - q u e r y
DispatchQueue . main . async { [ weak self ] in
self ? . startObservingChanges ( didReturnFromBackground : true )
}
}
@objc func applicationDidResignActive ( _ notification : Notification ) {
stopObservingChanges ( )
}
private func setupLayout ( ) {
contentStackView . pin ( to : view )
initialLoadLabel . pin ( . top , to : . top , of : self . view , withInset : Values . massiveSpacing )
initialLoadLabel . pin ( . leading , to : . leading , of : self . view , withInset : Values . mediumSpacing )
initialLoadLabel . pin ( . trailing , to : . trailing , of : self . view , withInset : - Values . mediumSpacing )
emptyStateLabel . pin ( . top , to : . top , of : self . view , withInset : Values . massiveSpacing )
emptyStateLabel . pin ( . leading , to : . leading , of : self . view , withInset : Values . mediumSpacing )
emptyStateLabel . pin ( . trailing , to : . trailing , of : self . view , withInset : - Values . mediumSpacing )
fadeView . pin ( . leading , to : . leading , of : self . view )
fadeView . pin ( . trailing , to : . trailing , of : self . view )
fadeView . pin ( . bottom , to : . bottom , of : self . view )
footerButton . center ( . horizontal , in : self . view )
footerButton . pin ( . bottom , to : . bottom , of : self . view . safeAreaLayoutGuide , withInset : - Values . smallSpacing )
}
// MARK: - U p d a t i n g
private func startObservingChanges ( didReturnFromBackground : Bool = false ) {
// S t a r t o b s e r v i n g f o r d a t a c h a n g e s
dataChangeCancellable = viewModel . tableDataPublisher
. receive ( on : DispatchQueue . main )
. sink (
receiveCompletion : { [ weak self ] result in
switch result {
case . failure ( let error ) :
let title : String = ( self ? . viewModel . title ? ? " unknown " . localized ( ) )
// I f w e g o t a n e r r o r t h e n t r y t o r e s t a r t t h e s t r e a m o n c e , o t h e r w i s e l o g t h e e r r o r
guard self ? . dataStreamJustFailed = = false else {
Log . error ( . cat , " Unable to recover database stream in ' \( title ) ' settings with error: \( error ) " )
return
}
Log . info ( . cat , " Atempting recovery for database stream in ' \( title ) ' settings with error: \( error ) " )
self ? . dataStreamJustFailed = true
self ? . startObservingChanges ( didReturnFromBackground : didReturnFromBackground )
case . finished : break
}
} ,
receiveValue : { [ weak self ] updatedData in
self ? . dataStreamJustFailed = false
self ? . handleDataUpdates ( updatedData )
}
)
// S o m e v i e w M o d e l ' s m a y n e e d t o r u n c u s t o m l o g i c a f t e r r e t u r n i n g f r o m t h e b a c k g r o u n d s o t r i g g e r t h a t h e r e
if didReturnFromBackground { viewModel . didReturnFromBackground ( ) }
}
private func stopObservingChanges ( ) {
// S t o p o b s e r v i n g d a t a b a s e c h a n g e s
dataChangeCancellable ? . cancel ( )
}
private func handleDataUpdates ( _ updatedData : [ SectionModel ] ) {
// D e t e r m i n e i f w e h a v e a n y i t e m s f o r t h e e m p t y s t a t e
let itemCount : Int = updatedData
. map { $0 . elements . count }
. reduce ( 0 , + )
// E n s u r e t h e r e l o a d s r u n w i t h o u t a n i m a t i o n s ( i f w e d o n ' t d o t h i s t h e c e l l s w i l l a n i m a t e
// i n f r o m a f r a m e o f C G R e c t . z e r o o n a t l e a s t t h e f i r s t l o a d )
UIView . performWithoutAnimation {
// U p d a t e t h e i n i t i a l / e m p t y s t a t e
initialLoadLabel . isHidden = true
emptyStateLabel . isHidden = ( itemCount > 0 )
// U p d a t e t h e c o n t e n t
viewModel . updateTableData ( updatedData )
tableView . reloadData ( )
hasLoadedInitialTableData = true
// C o m p l e t e p a g e l o a d i n g
isLoadingMore = false
autoLoadNextPageIfNeeded ( )
}
}
private func autoLoadNextPageIfNeeded ( ) {
guard
self . hasLoadedInitialTableData &&
! self . isAutoLoadingNextPage &&
! self . isLoadingMore
else { return }
self . isAutoLoadingNextPage = true
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + PagedData . autoLoadNextPageDelay ) { [ weak self ] in
self ? . isAutoLoadingNextPage = false
// N o t e : W e s o r t t h e h e a d e r s a s w e w a n t t o p r i o r i t i s e l o a d i n g n e w e r p a g e s o v e r o l d e r o n e s
let sections : [ ( Section , CGRect ) ] = ( self ? . viewModel . tableData
. enumerated ( )
. map { index , section in
( section . model , ( self ? . tableView . rectForHeader ( inSection : index ) ? ? . zero ) )
} )
. defaulting ( to : [ ] )
let shouldLoadMore : Bool = sections
. contains { section , headerRect in
section . style = = . loadMore &&
headerRect != . zero &&
( self ? . tableView . bounds . contains ( headerRect ) = = true )
}
guard shouldLoadMore else { return }
self ? . isLoadingMore = true
DispatchQueue . global ( qos : . userInitiated ) . async { [ weak self ] in
( self ? . viewModel as ? ( any PagedObservationSource ) ) ? . loadPageAfter ( )
}
}
}
// MARK: - B i n d i n g
private func setupBinding ( ) {
( viewModel as ? ( any NavigationItemSource ) ) ? . setupBindings (
viewController : self ,
disposables : & disposables
)
( viewModel as ? ( any NavigatableStateHolder ) ) ? . navigatableState . setupBindings (
viewController : self ,
disposables : & disposables
)
( viewModel as ? ErasedEditableStateHolder ) ? . isEditing
. receive ( on : DispatchQueue . main )
. sink { [ weak self , weak tableView ] isEditing in
UIView . animate ( withDuration : 0.25 ) {
self ? . setEditing ( isEditing , animated : true )
tableView ? . visibleCells
. compactMap { $0 as ? SessionCell }
. filter { $0 . interactionMode = = . editable || $0 . interactionMode = = . alwaysEditing }
. enumerated ( )
. forEach { index , cell in
cell . update (
isEditing : ( isEditing || cell . interactionMode = = . alwaysEditing ) ,
becomeFirstResponder : (
isEditing &&
index = = 0 &&
cell . interactionMode != . alwaysEditing
) ,
animated : true
)
}
tableView ? . beginUpdates ( )
tableView ? . endUpdates ( )
}
}
. store ( in : & disposables )
viewModel . bannerInfo
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] info in
switch info {
case . some ( let info ) :
self ? . infoBanner . update ( with : info )
self ? . infoBanner . isHidden = false
case . none : self ? . infoBanner . isHidden = true
}
}
. store ( in : & disposables )
viewModel . emptyStateTextPublisher
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] text in
self ? . emptyStateLabel . text = text
}
. store ( in : & disposables )
viewModel . footerView
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] footerView in
self ? . tableView . tableFooterView = footerView
}
. store ( in : & disposables )
viewModel . footerButtonInfo
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] buttonInfo in
if let buttonInfo : SessionButton . Info = buttonInfo {
self ? . footerButton . setTitle ( buttonInfo . title , for : . normal )
self ? . footerButton . style = buttonInfo . style
self ? . footerButton . isEnabled = buttonInfo . isEnabled
self ? . footerButton . set ( . width , greaterThanOrEqualTo : buttonInfo . minWidth )
self ? . footerButton . accessibilityIdentifier = buttonInfo . accessibility ? . identifier
self ? . footerButton . accessibilityLabel = buttonInfo . accessibility ? . label
}
self ? . onFooterTap = buttonInfo ? . onTap
self ? . fadeView . isHidden = ( buttonInfo = = nil )
self ? . footerButton . isHidden = ( buttonInfo = = nil )
// I f w e h a v e a f o o t e r B u t t o n t h e n w e w a n t t o m a n u a l l y c o n t r o l t h e c o n t e n t I n s e t
let window : UIWindow ? = UIApplication . shared . keyWindow
self ? . tableView . contentInsetAdjustmentBehavior = ( buttonInfo = = nil ? . automatic : . never )
self ? . tableView . contentInset = UIEdgeInsets (
top : 0 ,
left : 0 ,
bottom : {
switch ( buttonInfo , window ? . safeAreaInsets . bottom ) {
case ( . none , 0 ) : return Values . largeSpacing
case ( . none , _ ) : return 0
case ( . some , _ ) : return Values . footerGradientHeight ( window : window )
}
} ( ) ,
right : 0
)
}
. store ( in : & disposables )
}
@objc private func footerButtonTapped ( ) {
onFooterTap ? ( )
}
// MARK: - U I T a b l e V i e w D a t a S o u r c e
func numberOfSections ( in tableView : UITableView ) -> Int {
return self . viewModel . tableData . count
}
func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int {
return self . viewModel . tableData [ section ] . elements . count
}
func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell {
let section : SectionModel = viewModel . tableData [ indexPath . section ]
let info : SessionCell . Info < TableItem > = section . elements [ indexPath . row ]
let cell : UITableViewCell = tableView . dequeue ( type : viewModel . cellType . viewType . self , for : indexPath )
switch ( cell , info ) {
case ( let cell as SessionCell , _ ) :
cell . update ( with : info , using : viewModel . dependencies )
cell . update (
isEditing : ( self . isEditing || ( info . title ? . interaction = = . alwaysEditing ) ) ,
becomeFirstResponder : false ,
animated : false
)
switch viewModel {
case let editableStateHolder as ErasedEditableStateHolder :
cell . textPublisher
. sink ( receiveValue : { [ weak editableStateHolder ] text in
editableStateHolder ? . textChanged ( text , for : info . id )
} )
. store ( in : & cell . disposables )
default : break
}
case ( let cell as FullConversationCell , let threadInfo as SessionCell . Info < SessionThreadViewModel > ) :
cell . accessibilityIdentifier = info . accessibility ? . identifier
cell . isAccessibilityElement = ( info . accessibility != nil )
cell . update ( with : threadInfo . id , using : viewModel . dependencies )
default :
Log . error ( . cat , " [SessionTableViewController] Got invalid combination of cellType: \( viewModel . cellType ) and tableData: \( SessionCell . Info < TableItem > . self ) " )
}
return cell
}
func tableView ( _ tableView : UITableView , viewForHeaderInSection section : Int ) -> UIView ? {
let section : SectionModel = viewModel . tableData [ section ]
let result : SessionHeaderView = tableView . dequeueHeaderFooterView ( type : SessionHeaderView . self )
result . update (
title : section . model . title ,
style : section . model . style
)
return result
}
func tableView ( _ tableView : UITableView , viewForFooterInSection section : Int ) -> UIView ? {
let section : SectionModel = viewModel . tableData [ section ]
if let footerString = section . model . footer {
let result : SessionFooterView = tableView . dequeueHeaderFooterView ( type : SessionFooterView . self )
result . update ( title : footerString )
return result
}
return UIView ( )
}
// MARK: - U I T a b l e V i e w D e l e g a t e
func tableView ( _ tableView : UITableView , heightForHeaderInSection section : Int ) -> CGFloat {
return viewModel . tableData [ section ] . model . style . height
}
func tableView ( _ tableView : UITableView , heightForFooterInSection section : Int ) -> CGFloat {
let section : SectionModel = viewModel . tableData [ section ]
return ( section . model . footer = = nil ? 0 : UITableView . automaticDimension )
}
func tableView ( _ tableView : UITableView , estimatedHeightForRowAt indexPath : IndexPath ) -> CGFloat {
return UITableView . automaticDimension
}
func tableView ( _ tableView : UITableView , heightForRowAt indexPath : IndexPath ) -> CGFloat {
return UITableView . automaticDimension
}
func tableView ( _ tableView : UITableView , willDisplayHeaderView view : UIView , forSection section : Int ) {
guard self . hasLoadedInitialTableData && self . viewHasAppeared && ! self . isLoadingMore else { return }
let section : SectionModel = self . viewModel . tableData [ section ]
switch section . model . style {
case . loadMore :
self . isLoadingMore = true
DispatchQueue . global ( qos : . userInitiated ) . async { [ weak self ] in
( self ? . viewModel as ? ( any PagedObservationSource ) ) ? . loadPageAfter ( )
}
default : break
}
}
func tableView ( _ tableView : UITableView , canEditRowAt indexPath : IndexPath ) -> Bool {
return viewModel . canEditRow ( at : indexPath )
}
func tableView ( _ tableView : UITableView , willBeginEditingRowAt indexPath : IndexPath ) {
UIContextualAction . willBeginEditing ( indexPath : indexPath , tableView : tableView )
}
func tableView ( _ tableView : UITableView , didEndEditingRowAt indexPath : IndexPath ? ) {
UIContextualAction . didEndEditing ( indexPath : indexPath , tableView : tableView )
}
func tableView ( _ tableView : UITableView , leadingSwipeActionsConfigurationForRowAt indexPath : IndexPath ) -> UISwipeActionsConfiguration ? {
return viewModel . leadingSwipeActionsConfiguration ( forRowAt : indexPath , in : tableView , of : self )
}
func tableView ( _ tableView : UITableView , trailingSwipeActionsConfigurationForRowAt indexPath : IndexPath ) -> UISwipeActionsConfiguration ? {
return viewModel . trailingSwipeActionsConfiguration ( forRowAt : indexPath , in : tableView , of : self )
}
func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
tableView . deselectRow ( at : indexPath , animated : true )
let section : SectionModel = self . viewModel . tableData [ indexPath . section ]
let info : SessionCell . Info < TableItem > = section . elements [ indexPath . row ]
// D o n o t h i n g i f t h e i t e m i s d i s a b l e d
guard info . isEnabled else { return }
// G e t t h e v i e w t h a t w a s t a p p e d ( f o r p r e s e n t i n g o n i P a d )
let tappedView : UIView ? = {
guard let cell : SessionCell = tableView . cellForRow ( at : indexPath ) as ? SessionCell else {
return nil
}
// R e t r i e v e t h e l a s t t o u c h l o c a t i o n f r o m t h e c e l l
let touchLocation : UITouch ? = cell . lastTouchLocation
cell . lastTouchLocation = nil
switch ( info . leadingAccessory , info . trailingAccessory ) {
case ( _ , is SessionCell . AccessoryConfig . HighlightingBackgroundLabel ) :
return ( ! cell . trailingAccessoryView . isHidden ? cell . trailingAccessoryView : cell )
case ( is SessionCell . AccessoryConfig . HighlightingBackgroundLabel , _ ) :
return ( ! cell . leadingAccessoryView . isHidden ? cell . leadingAccessoryView : cell )
case ( _ , is SessionCell . AccessoryConfig . HighlightingBackgroundLabelAndRadio ) :
guard let touchLocation : UITouch = touchLocation else { return cell }
let localPoint : CGPoint = touchLocation . location ( in : cell . trailingAccessoryView . highlightingBackgroundLabel )
guard
! cell . trailingAccessoryView . isHidden &&
cell . trailingAccessoryView . highlightingBackgroundLabel . bounds . contains ( localPoint )
else { return ( ! cell . trailingAccessoryView . isHidden ? cell . trailingAccessoryView : cell ) }
return cell . trailingAccessoryView . highlightingBackgroundLabel
case ( is SessionCell . AccessoryConfig . HighlightingBackgroundLabelAndRadio , _ ) :
guard let touchLocation : UITouch = touchLocation else { return cell }
let localPoint : CGPoint = touchLocation . location ( in : cell . trailingAccessoryView . highlightingBackgroundLabel )
guard
! cell . leadingAccessoryView . isHidden &&
cell . leadingAccessoryView . highlightingBackgroundLabel . bounds . contains ( localPoint )
else { return ( ! cell . leadingAccessoryView . isHidden ? cell . leadingAccessoryView : cell ) }
return cell . leadingAccessoryView . highlightingBackgroundLabel
default :
return cell
}
} ( )
let maybeOldSelection : ( Int , SessionCell . Info < TableItem > ) ? = section . elements
. enumerated ( )
. first ( where : { index , info in
switch ( info . leadingAccessory , info . trailingAccessory ) {
case ( _ , let accessory as SessionCell . AccessoryConfig . Radio ) : return accessory . liveIsSelected ( )
case ( let accessory as SessionCell . AccessoryConfig . Radio , _ ) : return accessory . liveIsSelected ( )
case ( _ , let accessory as SessionCell . AccessoryConfig . HighlightingBackgroundLabelAndRadio ) :
return accessory . liveIsSelected ( )
case ( let accessory as SessionCell . AccessoryConfig . HighlightingBackgroundLabelAndRadio , _ ) :
return accessory . liveIsSelected ( )
default : return false
}
} )
let performAction : ( ) -> Void = { [ weak self , weak tappedView ] in
info . onTap ? ( )
info . onTapView ? ( tappedView )
self ? . manuallyReload ( indexPath : indexPath , section : section , info : info )
// U p d a t e t h e o l d s e l e c t i o n a s w e l l
if let oldSelection : ( index : Int , info : SessionCell . Info < TableItem > ) = maybeOldSelection {
self ? . manuallyReload (
indexPath : IndexPath (
row : oldSelection . index ,
section : indexPath . section
) ,
section : section ,
info : oldSelection . info
)
}
}
guard
let confirmationInfo : ConfirmationModal . Info = info . confirmationInfo ,
confirmationInfo . showCondition . shouldShow ( for : info . currentBoolValue )
else {
performAction ( )
return
}
// S h o w a c o n f i r m a t i o n m o d a l b e f o r e c o n t i n u i n g
let confirmationModal : ConfirmationModal = ConfirmationModal (
targetView : tappedView ,
info : confirmationInfo
. with ( onConfirm : { _ in performAction ( ) } )
)
present ( confirmationModal , animated : true , completion : nil )
}
private func manuallyReload (
indexPath : IndexPath ,
section : SectionModel ,
info : SessionCell . Info < TableItem >
) {
// T r y u p d a t e t h e e x i s t i n g c e l l t o h a v e a n i c e a n i m a t i o n i n s t e a d o f r e l o a d i n g t h e c e l l
if let existingCell : SessionCell = tableView . cellForRow ( at : indexPath ) as ? SessionCell {
existingCell . update ( with : info , isManualReload : true , using : viewModel . dependencies )
}
else {
tableView . reloadRows ( at : [ indexPath ] , with : . none )
}
}
}