// // NotificationViewModel+diffable.swift // Mastodon // // Created by sxiaojian on 2021/4/13. // import CoreData import CoreDataStack import os.log import UIKit extension NotificationViewModel { func setupDiffableDataSource( for tableView: UITableView, delegate: NotificationTableViewCellDelegate, dependency: NeedsDependency ) { let timestampUpdatePublisher = Timer.publish(every: 30.0, on: .main, in: .common) .autoconnect() .share() .eraseToAnyPublisher() guard let userid = activeMastodonAuthenticationBox.value?.userID else { return } diffableDataSource = NotificationSection.tableViewDiffableDataSource( for: tableView, timestampUpdatePublisher: timestampUpdatePublisher, managedObjectContext: context.managedObjectContext, delegate: delegate, dependency: dependency, requestUserID: userid ) } } extension NotificationViewModel: NSFetchedResultsControllerDelegate { func controllerWillChangeContent(_ controller: NSFetchedResultsController) { os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) } func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) guard let tableView = self.tableView else { return } guard let navigationBar = contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return } guard let diffableDataSource = self.diffableDataSource else { return } let predicate = fetchedResultsController.fetchRequest.predicate let parentManagedObjectContext = fetchedResultsController.managedObjectContext let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) managedObjectContext.parent = parentManagedObjectContext managedObjectContext.perform { let notifications: [MastodonNotification] = { let request = MastodonNotification.sortedFetchRequest request.returnsObjectsAsFaults = false request.predicate = predicate do { return try managedObjectContext.fetch(request) } catch { assertionFailure(error.localizedDescription) return [] } }() DispatchQueue.main.async { let oldSnapshot = diffableDataSource.snapshot() var newSnapshot = NSDiffableDataSourceSnapshot() newSnapshot.appendSections([.main]) newSnapshot.appendItems(notifications.map { NotificationItem.notification(objectID: $0.objectID) }, toSection: .main) if !notifications.isEmpty, self.noMoreNotification.value == false { newSnapshot.appendItems([.bottomLoader], toSection: .main) } guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else { diffableDataSource.apply(newSnapshot, animatingDifferences: false) self.isFetchingLatestNotification.value = false tableView.reloadData() return } diffableDataSource.apply(newSnapshot, animatingDifferences: false) { tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) tableView.contentOffset.y = tableView.contentOffset.y - difference.offset self.isFetchingLatestNotification.value = false } } } } private struct Difference { let item: T let sourceIndexPath: IndexPath let targetIndexPath: IndexPath let offset: CGFloat } private func calculateReloadSnapshotDifference( navigationBar: UINavigationBar, tableView: UITableView, oldSnapshot: NSDiffableDataSourceSnapshot, newSnapshot: NSDiffableDataSourceSnapshot ) -> Difference? { guard oldSnapshot.numberOfItems != 0 else { return nil } // old snapshot not empty. set source index path to first item if not match let sourceIndexPath = UIViewController.topVisibleTableViewCellIndexPath(in: tableView, navigationBar: navigationBar) ?? IndexPath(row: 0, section: 0) guard sourceIndexPath.row < oldSnapshot.itemIdentifiers(inSection: .main).count else { return nil } if oldSnapshot.itemIdentifiers.elementsEqual(newSnapshot.itemIdentifiers) { return nil } let timelineItem = oldSnapshot.itemIdentifiers(inSection: .main)[sourceIndexPath.row] guard let itemIndex = newSnapshot.itemIdentifiers(inSection: .main).firstIndex(of: timelineItem) else { return nil } let targetIndexPath = IndexPath(row: itemIndex, section: 0) let offset = UIViewController.tableViewCellOriginOffsetToWindowTop(in: tableView, at: sourceIndexPath, navigationBar: navigationBar) return Difference( item: timelineItem, sourceIndexPath: sourceIndexPath, targetIndexPath: targetIndexPath, offset: offset ) } }