2021-02-07 07:42:50 +01:00
//
// H o m e T i m e l i n e V i e w M o d e l + D i f f a b l e . s w i f t
// M a s t o d o n
//
// C r e a t e d b y s x i a o j i a n o n 2 0 2 1 / 2 / 7 .
//
import os . log
import UIKit
import CoreData
import CoreDataStack
2022-10-10 13:14:52 +02:00
import MastodonUI
2021-02-07 07:42:50 +01:00
extension HomeTimelineViewModel {
func setupDiffableDataSource (
2022-01-27 14:23:39 +01:00
tableView : UITableView ,
2021-03-03 09:12:48 +01:00
statusTableViewCellDelegate : StatusTableViewCellDelegate ,
2021-02-07 07:42:50 +01:00
timelineMiddleLoaderTableViewCellDelegate : TimelineMiddleLoaderTableViewCellDelegate
) {
2022-01-27 14:23:39 +01:00
diffableDataSource = StatusSection . diffableDataSource (
tableView : tableView ,
context : context ,
configuration : StatusSection . Configuration (
2022-10-09 14:07:57 +02:00
authContext : authContext ,
2022-01-27 14:23:39 +01:00
statusTableViewCellDelegate : statusTableViewCellDelegate ,
2022-02-15 12:44:45 +01:00
timelineMiddleLoaderTableViewCellDelegate : timelineMiddleLoaderTableViewCellDelegate ,
filterContext : . home ,
activeFilters : context . statusFilterService . $ activeFilters
2022-01-27 14:23:39 +01:00
)
2021-02-07 07:42:50 +01:00
)
2021-07-09 07:17:25 +02:00
// m a k e i n i t i a l s n a p s h o t a n i m a t i o n s m o o t h
2022-01-27 14:23:39 +01:00
var snapshot = NSDiffableDataSourceSnapshot < StatusSection , StatusItem > ( )
2021-06-22 11:49:07 +02:00
snapshot . appendSections ( [ . main ] )
diffableDataSource ? . apply ( snapshot )
2022-01-27 14:23:39 +01:00
fetchedResultsController . $ records
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] records in
guard let self = self else { return }
guard let diffableDataSource = self . diffableDataSource else { return }
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : incoming \( records . count ) objects " )
Task {
let start = CACurrentMediaTime ( )
defer {
let end = CACurrentMediaTime ( )
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : cost \( end - start , format : . fixed ( precision : 4 ) ) s to process \( records . count ) feeds " )
}
let oldSnapshot = diffableDataSource . snapshot ( )
var newSnapshot : NSDiffableDataSourceSnapshot < StatusSection , StatusItem > = {
let newItems = records . map { record in
StatusItem . feed ( record : record )
}
var snapshot = NSDiffableDataSourceSnapshot < StatusSection , StatusItem > ( )
snapshot . appendSections ( [ . main ] )
snapshot . appendItems ( newItems , toSection : . main )
return snapshot
} ( )
let parentManagedObjectContext = self . context . managedObjectContext
let managedObjectContext = NSManagedObjectContext ( concurrencyType : . privateQueueConcurrencyType )
managedObjectContext . parent = parentManagedObjectContext
try ? await managedObjectContext . perform {
let anchors : [ Feed ] = {
let request = Feed . sortedFetchRequest
request . predicate = NSCompoundPredicate ( andPredicateWithSubpredicates : [
Feed . hasMorePredicate ( ) ,
self . fetchedResultsController . predicate ,
] )
do {
return try managedObjectContext . fetch ( request )
} catch {
assertionFailure ( error . localizedDescription )
return [ ]
}
} ( )
let itemIdentifiers = newSnapshot . itemIdentifiers
for ( index , item ) in itemIdentifiers . enumerated ( ) {
guard case let . feed ( record ) = item else { continue }
guard anchors . contains ( where : { feed in feed . objectID = = record . objectID } ) else { continue }
let isLast = index + 1 = = itemIdentifiers . count
if isLast {
newSnapshot . insertItems ( [ . bottomLoader ] , afterItem : item )
} else {
newSnapshot . insertItems ( [ . feedLoader ( record : record ) ] , afterItem : item )
}
}
}
let hasChanges = newSnapshot . itemIdentifiers != oldSnapshot . itemIdentifiers
if ! hasChanges {
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : snapshot not changes " )
self . didLoadLatest . send ( )
return
} else {
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : snapshot has changes " )
}
guard let difference = await self . calculateReloadSnapshotDifference (
tableView : tableView ,
oldSnapshot : oldSnapshot ,
newSnapshot : newSnapshot
) else {
await self . updateSnapshotUsingReloadData ( snapshot : newSnapshot )
self . didLoadLatest . send ( )
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : applied new snapshot " )
return
}
await self . updateSnapshotUsingReloadData ( snapshot : newSnapshot )
await tableView . scrollToRow ( at : difference . targetIndexPath , at : . top , animated : false )
var contentOffset = await tableView . contentOffset
contentOffset . y = await tableView . contentOffset . y - difference . sourceDistanceToTableViewTopEdge
await tableView . setContentOffset ( contentOffset , animated : false )
self . didLoadLatest . send ( )
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : applied new snapshot " )
} // e n d T a s k
}
. store ( in : & disposeBag )
2021-02-07 07:42:50 +01:00
}
}
2022-01-27 14:23:39 +01:00
extension HomeTimelineViewModel {
2021-02-07 07:42:50 +01:00
2022-01-27 14:23:39 +01:00
@ MainActor func updateDataSource (
snapshot : NSDiffableDataSourceSnapshot < StatusSection , StatusItem > ,
animatingDifferences : Bool
) async {
diffableDataSource ? . apply ( snapshot , animatingDifferences : animatingDifferences )
2021-02-07 07:42:50 +01:00
}
2022-01-27 14:23:39 +01:00
@ MainActor func updateSnapshotUsingReloadData (
snapshot : NSDiffableDataSourceSnapshot < StatusSection , StatusItem >
) async {
if #available ( iOS 15.0 , * ) {
await self . diffableDataSource ? . applySnapshotUsingReloadData ( snapshot )
} else {
diffableDataSource ? . applySnapshot ( snapshot , animated : false , completion : nil )
}
2021-02-07 07:42:50 +01:00
}
2022-01-27 14:23:39 +01:00
struct Difference < T > {
2021-02-07 07:42:50 +01:00
let item : T
let sourceIndexPath : IndexPath
2022-01-27 14:23:39 +01:00
let sourceDistanceToTableViewTopEdge : CGFloat
2021-02-07 07:42:50 +01:00
let targetIndexPath : IndexPath
}
2022-01-27 14:23:39 +01:00
@ MainActor private func calculateReloadSnapshotDifference < S : Hashable , T : Hashable > (
2021-02-07 07:42:50 +01:00
tableView : UITableView ,
2022-01-27 14:23:39 +01:00
oldSnapshot : NSDiffableDataSourceSnapshot < S , T > ,
newSnapshot : NSDiffableDataSourceSnapshot < S , T >
2021-02-07 07:42:50 +01:00
) -> Difference < T > ? {
2022-01-27 14:23:39 +01:00
guard let sourceIndexPath = ( tableView . indexPathsForVisibleRows ? ? [ ] ) . sorted ( ) . first else { return nil }
let rectForSourceItemCell = tableView . rectForRow ( at : sourceIndexPath )
2022-07-14 20:46:48 +02:00
let sourceDistanceToTableViewTopEdge : CGFloat = {
if tableView . window != nil {
return tableView . convert ( rectForSourceItemCell , to : nil ) . origin . y - tableView . safeAreaInsets . top
} else {
return rectForSourceItemCell . origin . y - tableView . contentOffset . y - tableView . safeAreaInsets . top
}
} ( )
2022-01-27 14:23:39 +01:00
guard sourceIndexPath . section < oldSnapshot . numberOfSections ,
sourceIndexPath . row < oldSnapshot . numberOfItems ( inSection : oldSnapshot . sectionIdentifiers [ sourceIndexPath . section ] )
else { return nil }
2021-02-07 07:42:50 +01:00
2022-01-27 14:23:39 +01:00
let sectionIdentifier = oldSnapshot . sectionIdentifiers [ sourceIndexPath . section ]
let item = oldSnapshot . itemIdentifiers ( inSection : sectionIdentifier ) [ sourceIndexPath . row ]
2021-02-07 07:42:50 +01:00
2022-01-27 14:23:39 +01:00
guard let targetIndexPathRow = newSnapshot . indexOfItem ( item ) ,
let newSectionIdentifier = newSnapshot . sectionIdentifier ( containingItem : item ) ,
let targetIndexPathSection = newSnapshot . indexOfSection ( newSectionIdentifier )
else { return nil }
2021-02-07 07:42:50 +01:00
2022-01-27 14:23:39 +01:00
let targetIndexPath = IndexPath ( row : targetIndexPathRow , section : targetIndexPathSection )
2021-02-07 07:42:50 +01:00
return Difference (
2022-01-27 14:23:39 +01:00
item : item ,
2021-02-07 07:42:50 +01:00
sourceIndexPath : sourceIndexPath ,
2022-01-27 14:23:39 +01:00
sourceDistanceToTableViewTopEdge : sourceDistanceToTableViewTopEdge ,
targetIndexPath : targetIndexPath
2021-02-07 07:42:50 +01:00
)
}
}