From 80f9800fd6c8bb9009eb97bd31abbc11fbfd9bd0 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 29 Nov 2022 20:53:00 -0500 Subject: [PATCH] Completely replace all items when jumping to present --- .../Timeline/TimelineViewController.swift | 69 ++++++++++++------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index 28037cb0..deb71f19 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -343,21 +343,19 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro } private func insertPresentItemsIfNecessary(_ presentItems: [String]) { - var snapshot = dataSource.snapshot() + let snapshot = dataSource.snapshot() guard snapshot.indexOfSection(.statuses) != nil else { return } let currentItems = snapshot.itemIdentifiers(inSection: .statuses) if case .status(id: let firstID, state: _) = currentItems.first, - // if there's no overlap between presentItems and the existing items in the data source, insert the present items and prompt the user + // if there's no overlap between presentItems and the existing items in the data source, prompt the user !presentItems.contains(firstID) { - // remove any existing gap, if there is one - if let index = currentItems.lastIndex(of: .gap) { - snapshot.deleteItems(Array(currentItems[index...])) - } - snapshot.insertItems([.gap], beforeItem: currentItems.first!) - snapshot.insertItems(presentItems.map { .status(id: $0, state: .unknown) }, beforeItem: .gap) + // create a new snapshot to reset the timeline to the "present" state + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.statuses]) + snapshot.appendItems(presentItems.map { .status(id: $0, state: .unknown) }, toSection: .statuses) var config = ToastConfiguration(title: "Jump to present") config.edge = .top @@ -366,12 +364,34 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro config.action = { [unowned self] toast in toast.dismissToast(animated: true) + let origSnapshot = self.dataSource.snapshot() + let origItemAtTop: (Item, CGFloat)? + if let statusesSection = origSnapshot.indexOfSection(.statuses), + let indexPath = self.collectionView.indexPathsForVisibleItems.sorted().first(where: { $0.section == statusesSection }), + let cell = self.collectionView.cellForItem(at: indexPath), + let item = self.dataSource.itemIdentifier(for: indexPath) { + origItemAtTop = (item, cell.convert(.zero, to: self.view).y - self.view.safeAreaInsets.top) + } else { + origItemAtTop = nil + } + self.dataSource.apply(snapshot, animatingDifferences: true) { - // TODO: we can't set prevScrollOffsetBeforeScrollToTop here to allow undoing the scroll-to-top - // because that would involve scrolling through unmeasured-cell which fucks up the content offset values. - // we probably need a data-source aware implementation of scrollToTop which uses item & offset w/in item - // to track the restore position self.collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: true) + + var config = ToastConfiguration(title: "Go back") + config.edge = .top + config.systemImageName = "arrow.down" + config.dismissAutomaticallyAfter = 4 + config.action = { [unowned self] toast in + toast.dismissToast(animated: true) + // todo: it would be nice if we could animate this, but that doesn't work with the screen-position-maintaining stuff + if let (item, offset) = origItemAtTop { + self.applySnapshot(snapshot, maintainingScreenPosition: offset, ofItem: item) + } else { + self.dataSource.apply(origSnapshot, animatingDifferences: false) + } + } + self.showToast(configuration: config, animated: true) } } self.showToast(configuration: config, animated: true) @@ -380,32 +400,35 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro // NOTE: this only works when items are being inserted ABOVE the item to maintain private func applySnapshot(_ snapshot: NSDiffableDataSourceSnapshot, maintainingBottomRelativeScrollPositionOf itemToMaintain: Item) { - // use a snapshot of the collection view to hide the flicker as the content offset changes and then changes back - let snapshotView = collectionView.snapshotView(afterScreenUpdates: false)! - snapshotView.layer.zPosition = 1000 - snapshotView.frame = view.bounds - view.addSubview(snapshotView) - var firstItemAfterOriginalGapOffsetFromTop: CGFloat = 0 if let indexPath = dataSource.indexPath(for: itemToMaintain), let cell = collectionView.cellForItem(at: indexPath) { // subtract top safe area inset b/c scrollToItem at .top aligns the top of the cell to the top of the safe area firstItemAfterOriginalGapOffsetFromTop = cell.convert(.zero, to: view).y - view.safeAreaInsets.top } + applySnapshot(snapshot, maintainingScreenPosition: firstItemAfterOriginalGapOffsetFromTop, ofItem: itemToMaintain) + } + + private func applySnapshot(_ snapshot: NSDiffableDataSourceSnapshot, maintainingScreenPosition offsetFromTop: CGFloat, ofItem itemToMaintain: Item) { + // use a snapshot of the collection view to hide the flicker as the content offset changes and then changes back + let snapshotView = collectionView.snapshotView(afterScreenUpdates: false)! + snapshotView.layer.zPosition = 1000 + snapshotView.frame = view.bounds + view.addSubview(snapshotView) dataSource.apply(snapshot, animatingDifferences: false) { - if let indexPathOfItemAfterOriginalGap = self.dataSource.indexPath(for: itemToMaintain) { + if let indexPathOfItemToMaintain = self.dataSource.indexPath(for: itemToMaintain) { // scroll up until we've accumulated enough MEASURED height that we can put the // firstItemAfterOriginalGapCell at the top of the screen and then scroll down by // firstItemAfterOriginalGapOffsetFromTop without intruding into unmeasured area - var cur = indexPathOfItemAfterOriginalGap + var cur = indexPathOfItemToMaintain var amountScrolledUp: CGFloat = 0 while true { if cur.row <= 0 { break } - if let cell = self.collectionView.cellForItem(at: indexPathOfItemAfterOriginalGap), - cell.convert(.zero, to: self.view).y - self.view.safeAreaInsets.top > firstItemAfterOriginalGapOffsetFromTop { + if let cell = self.collectionView.cellForItem(at: indexPathOfItemToMaintain), + cell.convert(.zero, to: self.view).y - self.view.safeAreaInsets.top > offsetFromTop { break } cur = IndexPath(row: cur.row - 1, section: cur.section) @@ -415,7 +438,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro amountScrolledUp += attrs.size.height } self.collectionView.contentOffset.y += amountScrolledUp - self.collectionView.contentOffset.y -= firstItemAfterOriginalGapOffsetFromTop + self.collectionView.contentOffset.y -= offsetFromTop } snapshotView.removeFromSuperview()