Completely replace all items when jumping to present

This commit is contained in:
Shadowfacts 2022-11-29 20:53:00 -05:00
parent 0485400c1f
commit 80f9800fd6
1 changed files with 46 additions and 23 deletions

View File

@ -343,21 +343,19 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
} }
private func insertPresentItemsIfNecessary(_ presentItems: [String]) { private func insertPresentItemsIfNecessary(_ presentItems: [String]) {
var snapshot = dataSource.snapshot() let snapshot = dataSource.snapshot()
guard snapshot.indexOfSection(.statuses) != nil else { guard snapshot.indexOfSection(.statuses) != nil else {
return return
} }
let currentItems = snapshot.itemIdentifiers(inSection: .statuses) let currentItems = snapshot.itemIdentifiers(inSection: .statuses)
if case .status(id: let firstID, state: _) = currentItems.first, 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) { !presentItems.contains(firstID) {
// remove any existing gap, if there is one // create a new snapshot to reset the timeline to the "present" state
if let index = currentItems.lastIndex(of: .gap) { var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.deleteItems(Array(currentItems[index...])) snapshot.appendSections([.statuses])
} snapshot.appendItems(presentItems.map { .status(id: $0, state: .unknown) }, toSection: .statuses)
snapshot.insertItems([.gap], beforeItem: currentItems.first!)
snapshot.insertItems(presentItems.map { .status(id: $0, state: .unknown) }, beforeItem: .gap)
var config = ToastConfiguration(title: "Jump to present") var config = ToastConfiguration(title: "Jump to present")
config.edge = .top config.edge = .top
@ -366,12 +364,34 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
config.action = { [unowned self] toast in config.action = { [unowned self] toast in
toast.dismissToast(animated: true) 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) { 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) 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) 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 // NOTE: this only works when items are being inserted ABOVE the item to maintain
private func applySnapshot(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, maintainingBottomRelativeScrollPositionOf itemToMaintain: Item) { private func applySnapshot(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, 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 var firstItemAfterOriginalGapOffsetFromTop: CGFloat = 0
if let indexPath = dataSource.indexPath(for: itemToMaintain), if let indexPath = dataSource.indexPath(for: itemToMaintain),
let cell = collectionView.cellForItem(at: indexPath) { 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 // 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 firstItemAfterOriginalGapOffsetFromTop = cell.convert(.zero, to: view).y - view.safeAreaInsets.top
} }
applySnapshot(snapshot, maintainingScreenPosition: firstItemAfterOriginalGapOffsetFromTop, ofItem: itemToMaintain)
}
private func applySnapshot(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, 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) { 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 // 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 // firstItemAfterOriginalGapCell at the top of the screen and then scroll down by
// firstItemAfterOriginalGapOffsetFromTop without intruding into unmeasured area // firstItemAfterOriginalGapOffsetFromTop without intruding into unmeasured area
var cur = indexPathOfItemAfterOriginalGap var cur = indexPathOfItemToMaintain
var amountScrolledUp: CGFloat = 0 var amountScrolledUp: CGFloat = 0
while true { while true {
if cur.row <= 0 { if cur.row <= 0 {
break break
} }
if let cell = self.collectionView.cellForItem(at: indexPathOfItemAfterOriginalGap), if let cell = self.collectionView.cellForItem(at: indexPathOfItemToMaintain),
cell.convert(.zero, to: self.view).y - self.view.safeAreaInsets.top > firstItemAfterOriginalGapOffsetFromTop { cell.convert(.zero, to: self.view).y - self.view.safeAreaInsets.top > offsetFromTop {
break break
} }
cur = IndexPath(row: cur.row - 1, section: cur.section) cur = IndexPath(row: cur.row - 1, section: cur.section)
@ -415,7 +438,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
amountScrolledUp += attrs.size.height amountScrolledUp += attrs.size.height
} }
self.collectionView.contentOffset.y += amountScrolledUp self.collectionView.contentOffset.y += amountScrolledUp
self.collectionView.contentOffset.y -= firstItemAfterOriginalGapOffsetFromTop self.collectionView.contentOffset.y -= offsetFromTop
} }
snapshotView.removeFromSuperview() snapshotView.removeFromSuperview()