From 89b35fab6dca1b7dd84fe2346b3f10bc16451e1a Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 26 Oct 2020 22:55:58 -0400 Subject: [PATCH] Move pruning of offscreen rows to when the VC disappears, instead of during scrolling Prevents race when removing and adding cells in the willDisplay table view delegate method. --- Tusker.xcodeproj/project.pbxproj | 4 + Tusker/SceneDelegate.swift | 4 + .../Main/MainSplitViewController.swift | 14 +++ .../Main/MainTabBarViewController.swift | 8 ++ .../NotificationsTableViewController.swift | 81 +++++++----- .../ProfileStatusesViewController.swift | 2 - .../TimelineTableViewController.swift | 119 ++++++++++-------- .../BackgroundableViewController.swift | 13 ++ .../EnhancedNavigationViewController.swift | 8 ++ .../SegmentedPageViewController.swift | 8 ++ 10 files changed, 174 insertions(+), 87 deletions(-) create mode 100644 Tusker/Screens/Utilities/BackgroundableViewController.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 9fb44a5c..f5ede7f8 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; }; D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; }; D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; }; + D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; }; D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; }; D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; }; D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */; }; @@ -482,6 +483,7 @@ D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = ""; }; D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = ""; }; D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = ""; }; + D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundableViewController.swift; sourceTree = ""; }; D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = ""; }; @@ -1327,6 +1329,7 @@ D693DE5823FE24300061E07D /* InteractivePushTransition.swift */, D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */, D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */, + D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */, ); path = Utilities; sourceTree = ""; @@ -1864,6 +1867,7 @@ D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */, D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */, D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */, + D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */, D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */, D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */, D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */, diff --git a/Tusker/SceneDelegate.swift b/Tusker/SceneDelegate.swift index 27eccb6d..075f1c30 100644 --- a/Tusker/SceneDelegate.swift +++ b/Tusker/SceneDelegate.swift @@ -108,6 +108,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. + if let rootVC = window?.rootViewController as? BackgroundableViewController { + rootVC.sceneDidEnterBackground() + } + try! scene.session.mastodonController?.persistentContainer.viewContext.save() } diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index 1aec0f08..7a6fd28f 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -349,3 +349,17 @@ extension MainSplitViewController: TuskerRootViewController { } } } + +@available(iOS 14.0, *) +extension MainSplitViewController: BackgroundableViewController { + func sceneDidEnterBackground() { + if traitCollection.horizontalSizeClass == .compact { + tabBarViewController.sceneDidEnterBackground() + } else { + // todo: should this do the same for the sidebar VC as well? + if let contentVC = viewController(for: .secondary) as? BackgroundableViewController { + contentVC.sceneDidEnterBackground() + } + } + } +} diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 1f640d42..58ae420c 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -136,3 +136,11 @@ extension MainTabBarViewController: TuskerRootViewController { } } } + +extension MainTabBarViewController: BackgroundableViewController { + func sceneDidEnterBackground() { + if let selectedVC = selectedViewController as? BackgroundableViewController { + selectedVC.sceneDidEnterBackground() + } + } +} diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index d11cb1f9..c8c17461 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -19,15 +19,18 @@ class NotificationsTableViewController: EnhancedTableViewController { weak var mastodonController: MastodonController! - let excludedTypes: [Pachyderm.Notification.Kind] - let groupTypes = [Notification.Kind.favourite, .reblog, .follow] + private let excludedTypes: [Pachyderm.Notification.Kind] + private let groupTypes = [Notification.Kind.favourite, .reblog, .follow] private var loaded = false - var groups: [NotificationGroup] = [] + private var groups: [NotificationGroup] = [] - var newer: RequestRange? - var older: RequestRange? + private let pageSize = 20 + private var newer: RequestRange? + private var older: RequestRange? + + private var lastLastVisibleRow: IndexPath? init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) { self.excludedTypes = Array(Set(Pachyderm.Notification.Kind.allCases).subtracting(allowedTypes)) @@ -84,6 +87,41 @@ class NotificationsTableViewController: EnhancedTableViewController { } } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + pruneOffscreenRows() + } + + private func pruneOffscreenRows() { + guard let lastVisibleRow = lastLastVisibleRow else { + return + } + let lastRowIndex = groups.count - 1 + + if lastVisibleRow.row < lastRowIndex - pageSize { + // if there are more than 20 rows below the lats visible one + + let rowIndicesToRemove = (lastVisibleRow.row + pageSize).. Int { @@ -137,32 +175,7 @@ class NotificationsTableViewController: EnhancedTableViewController { // MARK: - Table view delegate override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - // see TimelineTableViewController.tableView(_:willDisplay:forRowAt:) - if !isCurrentlyScrollingToTop, scrollViewDirection < 0 { - let pageSize = 20 - if groups.count > 2 * pageSize, - indexPath.row < groups.count - (2 * pageSize) { - let groupsToRemove = groups[groups.count - pageSize.. 0, indexPath.section == timelineSegments.count, diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 171a9849..85914f7c 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -18,8 +18,11 @@ class TimelineTableViewController: EnhancedTableViewController, StatusTableViewC var timelineSegments: [[(id: String, state: StatusState)]] = [] - var newer: RequestRange? - var older: RequestRange? + private let pageSize = 20 + private var newer: RequestRange? + private var older: RequestRange? + + private var lastLastVisibleRow: IndexPath? init(for timeline: Timeline, mastodonController: MastodonController) { self.timeline = timeline @@ -73,6 +76,12 @@ class TimelineTableViewController: EnhancedTableViewController, StatusTableViewC loadInitialStatuses() } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + pruneOffscreenRows() + } + func loadInitialStatuses() { guard !loaded else { return } loaded = true @@ -91,6 +100,52 @@ class TimelineTableViewController: EnhancedTableViewController, StatusTableViewC } } } + + private func pruneOffscreenRows() { + guard let lastVisibleRow = lastLastVisibleRow else { + return + } + let lastSectionIndex = timelineSegments.count - 1 + + if lastVisibleRow.section < lastSectionIndex { + // if there is a section below the last visible one + + let sectionsToRemove = (lastVisibleRow.section + 1)...lastSectionIndex + + for section in sectionsToRemove { + for (id, _) in timelineSegments.remove(at: section) { + mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount() + } + } + + UIView.performWithoutAnimation { + tableView.deleteSections(IndexSet(sectionsToRemove), with: .none) + } + + } else if lastVisibleRow.section == lastSectionIndex { + let lastSection = timelineSegments.last! + let lastRowIndex = lastSection.count - 1 + + if lastVisibleRow.row < lastRowIndex - 20 { + // if there are more than 20 rows in the current section below the last visible one + + let rowIndicesInLastSectionToRemove = (lastVisibleRow.row + 20).. 2 * pageSize, - indexPath.row < lastSection.count - (2 * pageSize) { - // todo: this is in the hot path for scrolling, possibly move this to a background thread? - let statusesToRemove = lastSection[lastSection.count - pageSize..