From 5aa917e7bd2a03b7a5830bf7ca87eafa8a34c9e7 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Fri, 5 Feb 2021 16:50:40 +0800 Subject: [PATCH] fix : maintain contentOffset after refresh timeline --- Mastodon.xcodeproj/project.pbxproj | 4 ++ ...stableTimelineViewControllerDelegate.swift | 13 +++++ .../PublicTimelineViewController.swift | 9 +++ .../PublicTimelineViewModel+State.swift | 1 - .../PublicTimelineViewModel.swift | 55 ++++++++++++++++++- 5 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 Mastodon/Protocol/ContentOffsetAdjustableTimelineViewControllerDelegate.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index ebbffe843..78f471069 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; }; 2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; }; 2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */; }; + 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; }; 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; }; 2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; }; 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; }; @@ -161,6 +162,7 @@ 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; 2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; + 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = ""; }; 2D42FF6A25C817D2004A627A /* MastodonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonContent.swift; sourceTree = ""; }; 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = ""; }; 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = ""; }; @@ -371,6 +373,7 @@ isa = PBXGroup; children = ( 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */, + 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */, ); path = Protocol; sourceTree = ""; @@ -1099,6 +1102,7 @@ 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, + 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */, DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */, DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */, 2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */, diff --git a/Mastodon/Protocol/ContentOffsetAdjustableTimelineViewControllerDelegate.swift b/Mastodon/Protocol/ContentOffsetAdjustableTimelineViewControllerDelegate.swift new file mode 100644 index 000000000..dbe22c52e --- /dev/null +++ b/Mastodon/Protocol/ContentOffsetAdjustableTimelineViewControllerDelegate.swift @@ -0,0 +1,13 @@ +// +// ContentOffsetAdjustableTimelineViewControllerDelegate.swift +// Mastodon +// +// Created by sxiaojian on 2021/2/5. +// + +import UIKit + +protocol ContentOffsetAdjustableTimelineViewControllerDelegate: class { + func navigationBar() -> UINavigationBar? +} + diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift index cb8b67f12..b83d296e3 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift @@ -70,6 +70,7 @@ extension PublicTimelineViewController { ]) viewModel.tableView = tableView + viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self tableView.delegate = self viewModel.setupDiffableDataSource( for: tableView, @@ -122,6 +123,14 @@ extension PublicTimelineViewController: UITableViewDelegate { viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key)) } } + +// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate +extension PublicTimelineViewController: ContentOffsetAdjustableTimelineViewControllerDelegate { + func navigationBar() -> UINavigationBar? { + return navigationController?.navigationBar + } +} + // MARK: - LoadMoreConfigurableTableViewContainer extension PublicTimelineViewController: LoadMoreConfigurableTableViewContainer { typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift index e9182dfe5..258db0cdd 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift @@ -68,7 +68,6 @@ extension PublicTimelineViewModel.State { break } } receiveValue: { response in - viewModel.isFetchingLatestTimeline.value = false let resposeTootIDs = response.value.compactMap { $0.id } var newTootsIDs = resposeTootIDs let oldTootsIDs = viewModel.tootIDs.value diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift index 369d771e8..42590a919 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift @@ -27,6 +27,9 @@ class PublicTimelineViewModel: NSObject { let loadMiddleSateMachineList = CurrentValueSubject<[String: GKStateMachine], Never>([:]) weak var tableView: UITableView? + + weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? + // var tootIDsWhichHasGap = [String]() // output @@ -74,6 +77,9 @@ class PublicTimelineViewModel: NSObject { .sink { [weak self] items in guard let self = self else { return } guard let diffableDataSource = self.diffableDataSource else { return } + guard let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return } + guard let tableView = self.tableView else { return } + let oldSnapshot = diffableDataSource.snapshot() os_log("%{public}s[%{public}ld], %{public}s: items did change", (#file as NSString).lastPathComponent, #line, #function) var snapshot = NSDiffableDataSourceSnapshot() @@ -87,7 +93,21 @@ class PublicTimelineViewModel: NSObject { break } } - diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty) + + DispatchQueue.main.async { + + guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: snapshot) else { + diffableDataSource.apply(snapshot) + self.isFetchingLatestTimeline.value = false + return + } + + diffableDataSource.apply(snapshot, animatingDifferences: false) { + tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) + tableView.contentOffset.y = tableView.contentOffset.y - difference.offset + self.isFetchingLatestTimeline.value = false + } + } } .store(in: &disposeBag) @@ -109,4 +129,37 @@ class PublicTimelineViewModel: NSObject { deinit { os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) } + + private struct Difference { + let item: T + let sourceIndexPath: IndexPath + let targetIndexPath: IndexPath + let offset: CGFloat + } + + private func calculateReloadSnapshotDifference( + navigationBar: UINavigationBar, + tableView: UITableView, + oldSnapshot: NSDiffableDataSourceSnapshot, + newSnapshot: NSDiffableDataSourceSnapshot + ) -> Difference? { + guard oldSnapshot.numberOfItems != 0 else { return nil } + + // old snapshot not empty. set source index path to first item if not match + let sourceIndexPath = UIViewController.topVisibleTableViewCellIndexPath(in: tableView, navigationBar: navigationBar) ?? IndexPath(row: 0, section: 0) + + guard sourceIndexPath.row < oldSnapshot.itemIdentifiers(inSection: .main).count else { return nil } + + let timelineItem = oldSnapshot.itemIdentifiers(inSection: .main)[sourceIndexPath.row] + guard let itemIndex = newSnapshot.itemIdentifiers(inSection: .main).firstIndex(of: timelineItem) else { return nil } + let targetIndexPath = IndexPath(row: itemIndex, section: 0) + + let offset = UIViewController.tableViewCellOriginOffsetToWindowTop(in: tableView, at: sourceIndexPath, navigationBar: navigationBar) + return Difference( + item: timelineItem, + sourceIndexPath: sourceIndexPath, + targetIndexPath: targetIndexPath, + offset: offset + ) + } }