diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index 5de6b920..8d4a7eec 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -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 {