Change timeline gap-filling to do a proper job of maintaining the bottom-relative scroll position
This commit is contained in:
parent
276691efbf
commit
47b838a386
|
@ -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) {}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue