More fiddling with how Jump to Present works

Now, when loading present items, they're inserted into the data source
immediately along with a gap. If the user taps Jump to Present, then a
new snapshot _with only the present items_ will be applied (which allows
infinite scrolling to work properly when they scroll back down) and the
view scrolled-to-top. Tapping Go Back, then, applies the original
snapshot (i.e., the current one from when Jump to Present was tapped)
and restores the scroll position.
This commit is contained in:
Shadowfacts 2022-12-05 17:08:24 -05:00
parent 97dec0f9d2
commit 0ce57d1308
1 changed files with 50 additions and 14 deletions

View File

@ -412,7 +412,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
// I'm not sure whether this should move into TimelineLikeController/TimelineLikeCollectionViewController // I'm not sure whether this should move into TimelineLikeController/TimelineLikeCollectionViewController
let (_, presentItems) = await (loadNewerAndEndRefreshing(), try? loadInitial()) let (_, presentItems) = await (loadNewerAndEndRefreshing(), try? loadInitial())
if let presentItems, !presentItems.isEmpty { if let presentItems, !presentItems.isEmpty {
insertPresentItemsIfNecessary(presentItems) insertPresentItemsAndShowJumpToast(presentItems)
} }
} }
} }
@ -430,13 +430,13 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
UIAccessibility.post(notification: .screenChanged, argument: self.collectionView.cellForItem(at: IndexPath(row: 0, section: 0))) UIAccessibility.post(notification: .screenChanged, argument: self.collectionView.cellForItem(at: IndexPath(row: 0, section: 0)))
} }
} else { } else {
insertPresentItemsIfNecessary(presentItems) insertPresentItemsAndShowJumpToast(presentItems)
} }
} }
} }
private func insertPresentItemsIfNecessary(_ presentItems: [String]) { private func insertPresentItemsAndShowJumpToast(_ presentItems: [String]) {
let snapshot = dataSource.snapshot() var snapshot = dataSource.snapshot()
guard snapshot.indexOfSection(.statuses) != nil else { guard snapshot.indexOfSection(.statuses) != nil else {
return return
} }
@ -444,13 +444,38 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
if case .status(id: let firstID, _, _) = currentItems.first, if case .status(id: let firstID, _, _) = currentItems.first,
// if there's no overlap between presentItems and the existing items in the data source, 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) {
let applySnapshotBeforeScrolling: Bool
// create a new snapshot to reset the timeline to the "present" state // remove any existing gap, if there is one
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() if let index = currentItems.lastIndex(of: .gap) {
snapshot.appendSections([.statuses]) snapshot.deleteItems(Array(currentItems[index...]))
snapshot.appendItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, toSection: .statuses)
var config = ToastConfiguration(title: "Jump to present") let statusesSection = snapshot.indexOfSection(.statuses)!
if collectionView.indexPathsForVisibleItems.contains(IndexPath(row: index, section: statusesSection)) {
// the gap cell is on screen
applySnapshotBeforeScrolling = false
} else if let topMostVisibleCell = collectionView.indexPathsForVisibleItems.first(where: { $0.section == statusesSection }),
index < topMostVisibleCell.row {
// the gap cell is above the top, so applying the snapshot would remove the currently-viewed statuses
applySnapshotBeforeScrolling = false
} else {
// the gap cell is below the bottom of the screen
applySnapshotBeforeScrolling = true
}
} else {
// there is no existing gap
applySnapshotBeforeScrolling = true
}
snapshot.insertItems([.gap], beforeItem: currentItems.first!)
snapshot.insertItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, beforeItem: .gap)
if applySnapshotBeforeScrolling {
let firstVisibleIndexPath = collectionView.indexPathsForVisibleItems.min()!
let firstVisibleItem = dataSource.itemIdentifier(for: firstVisibleIndexPath)!
applySnapshot(snapshot, maintainingBottomRelativeScrollPositionOf: firstVisibleItem)
}
var config = ToastConfiguration(title: "Jump to Present")
config.edge = .top config.edge = .top
config.systemImageName = "arrow.up" config.systemImageName = "arrow.up"
config.dismissAutomaticallyAfter = 4 config.dismissAutomaticallyAfter = 4
@ -468,18 +493,23 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
origItemAtTop = nil origItemAtTop = nil
} }
self.dataSource.apply(snapshot, animatingDifferences: true) { // when the user explicitly taps Jump to Present, we drop all the old items to let infinite scrolling work properly when they scroll back down
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.statuses])
snapshot.appendItems(presentItems.map { .status(id: $0, collapseState: .unknown, filterState: .unknown) }, toSection: .statuses)
// don't animate the snapshot change, the scrolling animation will paper over the switch
self.dataSource.apply(snapshot, animatingDifferences: false) {
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") var config = ToastConfiguration(title: "Go Back")
config.edge = .top config.edge = .top
config.systemImageName = "arrow.down" config.systemImageName = "arrow.down"
config.dismissAutomaticallyAfter = 4 config.dismissAutomaticallyAfter = 2
config.action = { [unowned self] toast in config.action = { [unowned self] toast in
toast.dismissToast(animated: true) 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 // 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 { if let (item, offset) = origItemAtTop {
self.applySnapshot(snapshot, maintainingScreenPosition: offset, ofItem: item) self.applySnapshot(origSnapshot, maintainingScreenPosition: offset, ofItem: item)
} else { } else {
self.dataSource.apply(origSnapshot, animatingDifferences: false) self.dataSource.apply(origSnapshot, animatingDifferences: false)
} }
@ -516,12 +546,17 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
// firstItemAfterOriginalGapOffsetFromTop without intruding into unmeasured area // firstItemAfterOriginalGapOffsetFromTop without intruding into unmeasured area
var cur = indexPathOfItemToMaintain var cur = indexPathOfItemToMaintain
var amountScrolledUp: CGFloat = 0 var amountScrolledUp: CGFloat = 0
var first = true
while true { while true {
if cur.row <= 0 { if cur.row <= 0 {
break break
} }
if let cell = self.collectionView.cellForItem(at: indexPathOfItemToMaintain), if let cell = self.collectionView.cellForItem(at: indexPathOfItemToMaintain),
cell.convert(.zero, to: self.view).y - self.view.safeAreaInsets.top > offsetFromTop { cell.convert(.zero, to: self.view).y - self.view.safeAreaInsets.top > offsetFromTop {
// if we're breaking from the loop at the first iteration, we need to make sure to still call scrollToItem for the current row
if first {
self.collectionView.scrollToItem(at: cur, at: .top, animated: false)
}
break break
} }
cur = IndexPath(row: cur.row - 1, section: cur.section) cur = IndexPath(row: cur.row - 1, section: cur.section)
@ -529,6 +564,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
self.collectionView.layoutIfNeeded() self.collectionView.layoutIfNeeded()
let attrs = self.collectionView.layoutAttributesForItem(at: cur)! let attrs = self.collectionView.layoutAttributesForItem(at: cur)!
amountScrolledUp += attrs.size.height amountScrolledUp += attrs.size.height
first = false
} }
self.collectionView.contentOffset.y += amountScrolledUp self.collectionView.contentOffset.y += amountScrolledUp
self.collectionView.contentOffset.y -= offsetFromTop self.collectionView.contentOffset.y -= offsetFromTop