Show jump to present toast if necessary when scene re-appears

This commit is contained in:
Shadowfacts 2022-11-19 13:09:37 -05:00
parent ce534c4a05
commit 9ff1452c68
1 changed files with 80 additions and 41 deletions

View File

@ -125,6 +125,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
cell.update()
}
}
NotificationCenter.default.addObserver(self, selector: #selector(sceneWillEnterForeground), name: UIScene.willEnterForegroundNotification, object: nil)
}
// separate method because InstanceTimelineViewController needs to be able to customize it
@ -242,6 +244,20 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
// }
// }
@objc private func sceneWillEnterForeground(_ notification: Foundation.Notification) {
guard let scene = notification.object as? UIScene,
// view.window is nil when the VC is not on screen
view.window?.windowScene == scene else {
return
}
Task {
if case .idle = controller.state,
let presentItems = try? await loadInitial() {
insertPresentItemsIfNecessary(presentItems)
}
}
}
@objc func refresh() {
Task {
if case .notLoadedInitial = controller.state {
@ -249,47 +265,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
} else {
// I'm not sure whether this should move into TimelineLikeController/TimelineLikeCollectionViewController
let (_, presentItems) = await (controller.loadNewer(), try? loadInitial())
if let presentItems,
case .status(id: let id, state: _) = dataSource.snapshot().itemIdentifiers(inSection: .statuses).first {
// if there's no overlap between presentItems and the existing items in the data source, prompt the user to scroll to present
if !presentItems.contains(id) {
var snapshot = self.dataSource.snapshot()
let currentItems = snapshot.itemIdentifiers(inSection: .statuses)
guard let item = currentItems.first,
case .status(id: id, state: _) = item else {
return
}
// remove any existing gap if there is one
if let index = currentItems.lastIndex(of: .gap) {
snapshot.deleteItems(Array(currentItems[index...]))
}
snapshot.insertItems([.gap], beforeItem: item)
snapshot.insertItems(presentItems.map { .status(id: $0, state: .unknown) }, beforeItem: .gap)
// 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()
}
var config = ToastConfiguration(title: "Jump to present")
config.edge = .top
config.systemImageName = "arrow.up"
config.dismissAutomaticallyAfter = 4
config.action = { [unowned self] toast in
toast.dismissToast(animated: true)
self.collectionView.scrollToTop()
}
self.showToast(configuration: config, animated: true)
}
if let presentItems {
insertPresentItemsIfNecessary(presentItems)
}
}
#if !targetEnvironment(macCatalyst)
@ -298,6 +275,68 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
}
}
private func insertPresentItemsIfNecessary(_ presentItems: [String]) {
var snapshot = dataSource.snapshot()
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
!presentItems.contains(firstID) {
let applySnapshotBeforeScrolling: Bool
// remove any existing gap, if there is one
if let index = currentItems.lastIndex(of: .gap) {
snapshot.deleteItems(Array(currentItems[index...]))
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, 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()
}
}
var config = ToastConfiguration(title: "Jump to present")
config.edge = .top
config.systemImageName = "arrow.up"
config.dismissAutomaticallyAfter = 4
config.action = { [unowned self] toast in
toast.dismissToast(animated: true)
if !applySnapshotBeforeScrolling {
self.dataSource.apply(snapshot, animatingDifferences: false)
}
self.collectionView.scrollToTop()
}
self.showToast(configuration: config, animated: true)
}
}
}
extension TimelineViewController {