Change timeline gap-filling to do a proper job of maintaining the bottom-relative scroll position

This commit is contained in:
Shadowfacts 2022-11-21 22:47:44 -05:00
parent 276691efbf
commit 47b838a386
1 changed files with 49 additions and 34 deletions

View File

@ -246,17 +246,9 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
snapshot.insertItems(presentItems.map { .status(id: $0, state: .unknown) }, beforeItem: .gap)
if applySnapshotBeforeScrolling {
// 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)
let bottomOffset = collectionView.contentSize.height - collectionView.contentOffset.y
self.dataSource.apply(snapshot, animatingDifferences: false) {
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentSize.height - bottomOffset)
snapshotView.removeFromSuperview()
}
let firstVisibleIndexPath = collectionView.indexPathsForVisibleItems.min()!
let firstVisibleItem = dataSource.itemIdentifier(for: firstVisibleIndexPath)!
applySnapshot(snapshot, maintainingBottomRelativeScrollPositionOf: firstVisibleItem)
}
var config = ToastConfiguration(title: "Jump to present")
@ -276,6 +268,50 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
}
}
// NOTE: this only works when items are being inserted ABOVE the item to maintain
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
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
}
dataSource.apply(snapshot, animatingDifferences: false) {
if let indexPathOfItemAfterOriginalGap = 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 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 {
break
}
cur = IndexPath(row: cur.row - 1, section: cur.section)
self.collectionView.scrollToItem(at: cur, at: .top, animated: false)
self.collectionView.layoutIfNeeded()
let attrs = self.collectionView.layoutAttributesForItem(at: cur)!
amountScrolledUp += attrs.size.height
}
self.collectionView.contentOffset.y += amountScrolledUp
self.collectionView.contentOffset.y -= firstItemAfterOriginalGapOffsetFromTop
}
snapshotView.removeFromSuperview()
}
}
}
extension TimelineViewController {
@ -539,29 +575,8 @@ extension TimelineViewController {
}
if addedItems {
// 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)
// Yes, these are all load bearing. Setting the contentOffset seems to cause the collection view to recalculate the size
// of some cells, thus changing the contentSize and the offset necessary to match to match the bottom offset.
// Three DispatchQueue.main.async's seems to be the fewest we can reliably get away with.
let bottomOffset = collectionView.contentSize.height - collectionView.contentOffset.y
dataSource.apply(snapshot, animatingDifferences: false) {
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentSize.height - bottomOffset)
DispatchQueue.main.async {
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentSize.height - bottomOffset)
DispatchQueue.main.async {
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentSize.height - bottomOffset)
DispatchQueue.main.async {
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentSize.height - bottomOffset)
snapshotView.removeFromSuperview()
}
}
}
}
let firstItemAfterOriginalGap = statusItems[gapIndex + 1]
applySnapshot(snapshot, maintainingBottomRelativeScrollPositionOf: firstItemAfterOriginalGap)
} else {
dataSource.apply(snapshot, animatingDifferences: true) {}
}