From 2d65bda7fe4dec0a7b31b0a0bb88a3f1a88902d4 Mon Sep 17 00:00:00 2001 From: jk234ert Date: Wed, 7 Apr 2021 16:37:05 +0800 Subject: [PATCH] chore: migrate HashtagViewModel to use `StatusFetchedResultsController` --- Mastodon.xcodeproj/project.pbxproj | 4 - .../StatusFetchedResultsController.swift | 11 +-- .../StatusWithGapFetchResultController.swift | 85 ------------------- .../HashtagTimelineViewController.swift | 2 + .../HashtagTimelineViewModel+Diffable.swift | 23 ++--- ...tagTimelineViewModel+LoadLatestState.swift | 12 +-- ...tagTimelineViewModel+LoadMiddleState.swift | 16 ++-- ...tagTimelineViewModel+LoadOldestState.swift | 12 +-- .../HashtagTimelineViewModel.swift | 38 ++------- 9 files changed, 42 insertions(+), 161 deletions(-) delete mode 100644 Mastodon/Diffiable/FetchedResultsController/StatusWithGapFetchResultController.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 9af5bd234..824b60ad1 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 0F1E2D0B2615C39400C38565 /* HashtagTimelineTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F1E2D0A2615C39400C38565 /* HashtagTimelineTitleView.swift */; }; - 0F1E2FE9261D7FD000C38565 /* StatusWithGapFetchResultController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F1E2FE8261D7FD000C38565 /* StatusWithGapFetchResultController.swift */; }; 0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */; }; 0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */; }; 0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */; }; @@ -357,7 +356,6 @@ /* Begin PBXFileReference section */ 0F1E2D0A2615C39400C38565 /* HashtagTimelineTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineTitleView.swift; sourceTree = ""; }; - 0F1E2FE8261D7FD000C38565 /* StatusWithGapFetchResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusWithGapFetchResultController.swift; sourceTree = ""; }; 0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = ""; }; 0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewModel.swift; sourceTree = ""; }; 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+Diffable.swift"; sourceTree = ""; }; @@ -1661,7 +1659,6 @@ isa = PBXGroup; children = ( DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */, - 0F1E2FE8261D7FD000C38565 /* StatusWithGapFetchResultController.swift */, ); path = FetchedResultsController; sourceTree = ""; @@ -2140,7 +2137,6 @@ DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */, 2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */, - 0F1E2FE9261D7FD000C38565 /* StatusWithGapFetchResultController.swift in Sources */, DBCC3B7B261443AD0045B23D /* ViewController.swift in Sources */, 2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */, 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */, diff --git a/Mastodon/Diffiable/FetchedResultsController/StatusFetchedResultsController.swift b/Mastodon/Diffiable/FetchedResultsController/StatusFetchedResultsController.swift index a61429ab8..dd373b29f 100644 --- a/Mastodon/Diffiable/FetchedResultsController/StatusFetchedResultsController.swift +++ b/Mastodon/Diffiable/FetchedResultsController/StatusFetchedResultsController.swift @@ -25,7 +25,7 @@ final class StatusFetchedResultsController: NSObject { // output let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) - init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate) { + init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) { self.domain.value = domain ?? "" self.fetchedResultsController = { let fetchRequest = Status.sortedFetchRequest @@ -52,10 +52,11 @@ final class StatusFetchedResultsController: NSObject { .receive(on: DispatchQueue.main) .sink { [weak self] domain, ids in guard let self = self else { return } - self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ - Status.predicate(domain: domain ?? "", ids: ids), - additionalTweetPredicate - ]) + var predicates = [Status.predicate(domain: domain ?? "", ids: ids)] + if let additionalPredicate = additionalTweetPredicate { + predicates.append(additionalPredicate) + } + self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) do { try self.fetchedResultsController.performFetch() } catch { diff --git a/Mastodon/Diffiable/FetchedResultsController/StatusWithGapFetchResultController.swift b/Mastodon/Diffiable/FetchedResultsController/StatusWithGapFetchResultController.swift deleted file mode 100644 index f392c893d..000000000 --- a/Mastodon/Diffiable/FetchedResultsController/StatusWithGapFetchResultController.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// StatusWithGapFetchResultController.swift -// Mastodon -// -// Created by BradGao on 2021/4/7. -// - -import os.log -import UIKit -import Combine -import CoreData -import CoreDataStack -import MastodonSDK - -class StatusWithGapFetchResultController: NSObject { - var disposeBag = Set() - - let fetchedResultsController: NSFetchedResultsController - - // input - let domain = CurrentValueSubject(nil) - let statusIDs = CurrentValueSubject<[Mastodon.Entity.Status.ID], Never>([]) - - var needLoadMiddleIndex: Int? = nil - - // output - let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) - - init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate) { - self.domain.value = domain ?? "" - self.fetchedResultsController = { - let fetchRequest = Status.sortedFetchRequest - fetchRequest.returnsObjectsAsFaults = false - fetchRequest.fetchBatchSize = 20 - let controller = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: managedObjectContext, - sectionNameKeyPath: nil, - cacheName: nil - ) - - return controller - }() - super.init() - - fetchedResultsController.delegate = self - - Publishers.CombineLatest( - self.domain.removeDuplicates().eraseToAnyPublisher(), - self.statusIDs.removeDuplicates().eraseToAnyPublisher() - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] domain, ids in - guard let self = self else { return } - self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ - Status.predicate(domain: domain ?? "", ids: ids), - additionalTweetPredicate - ]) - do { - try self.fetchedResultsController.performFetch() - } catch { - assertionFailure(error.localizedDescription) - } - } - .store(in: &disposeBag) - } -} - -// MARK: - NSFetchedResultsControllerDelegate -extension StatusWithGapFetchResultController: NSFetchedResultsControllerDelegate { - func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - - let indexes = statusIDs.value - let objects = fetchedResultsController.fetchedObjects ?? [] - - let items: [NSManagedObjectID] = objects - .compactMap { object in - indexes.firstIndex(of: object.id).map { index in (index, object) } - } - .sorted { $0.0 < $1.0 } - .map { $0.1.objectID } - self.objectIDs.value = items - } -} diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index 1dbb0323c..9d638e6c6 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -107,6 +107,8 @@ extension HashtagTimelineViewController { self?.updatePromptTitle() } .store(in: &disposeBag) + + } diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift index a0bf5d82d..26f32a33c 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift @@ -33,14 +33,9 @@ extension HashtagTimelineViewModel { } } -// MARK: - NSFetchedResultsControllerDelegate -extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate { - - func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - - func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { +// MARK: - Compare old & new snapshots and generate new items +extension HashtagTimelineViewModel { + func generateStatusItems(newObjectIDs: [NSManagedObjectID]) { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) guard let tableView = self.tableView else { return } @@ -48,12 +43,12 @@ extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate { guard let diffableDataSource = self.diffableDataSource else { return } - let parentManagedObjectContext = fetchedResultsController.managedObjectContext + let parentManagedObjectContext = fetchedResultsController.fetchedResultsController.managedObjectContext let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) managedObjectContext.parent = parentManagedObjectContext let oldSnapshot = diffableDataSource.snapshot() - let snapshot = snapshot as NSDiffableDataSourceSnapshot +// let snapshot = snapshot as NSDiffableDataSourceSnapshot var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:] for item in oldSnapshot.itemIdentifiers { @@ -61,9 +56,7 @@ extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate { oldSnapshotAttributeDict[objectID] = attribute } - let statusItemList: [Item] = snapshot.itemIdentifiers.map { - let status = managedObjectContext.object(with: $0) as! Status - + let statusItemList: [Item] = newObjectIDs.map { let attribute = oldSnapshotAttributeDict[$0] ?? Item.StatusAttribute() return Item.status(objectID: $0, attribute: attribute) } @@ -75,7 +68,7 @@ extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate { if let needLoadMiddleIndex = needLoadMiddleIndex, needLoadMiddleIndex < (statusItemList.count - 1) { // If yes, insert a `middleLoader` at the index var newItems = statusItemList - newItems.insert(.homeMiddleLoader(upperTimelineIndexAnchorObjectID: snapshot.itemIdentifiers[needLoadMiddleIndex]), at: (needLoadMiddleIndex + 1)) + newItems.insert(.homeMiddleLoader(upperTimelineIndexAnchorObjectID: newObjectIDs[needLoadMiddleIndex]), at: (needLoadMiddleIndex + 1)) newSnapshot.appendItems(newItems, toSection: .main) } else { newSnapshot.appendItems(statusItemList, toSection: .main) @@ -112,6 +105,7 @@ extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate { newSnapshot: NSDiffableDataSourceSnapshot ) -> Difference? { guard oldSnapshot.numberOfItems != 0 else { return nil } + guard let item = oldSnapshot.itemIdentifiers.first as? Item, case Item.status = item else { return nil } let oldItemAtBeginning = oldSnapshot.itemIdentifiers(inSection: .main).first! @@ -127,5 +121,4 @@ extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate { } return nil } - } diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadLatestState.swift index d8e286195..e772e8ea0 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadLatestState.swift @@ -73,18 +73,18 @@ extension HashtagTimelineViewModel.LoadLatestState { // 1. is not empty // 2. last status are not recorded // Then we may have middle data to load - if !viewModel.hashtagStatusIDList.isEmpty, let lastNewStatusID = newStatusIDList.last, - !viewModel.hashtagStatusIDList.contains(lastNewStatusID) { + var oldStatusIDs = viewModel.fetchedResultsController.statusIDs.value + if !oldStatusIDs.isEmpty, let lastNewStatusID = newStatusIDList.last, + !oldStatusIDs.contains(lastNewStatusID) { viewModel.needLoadMiddleIndex = (newStatusIDList.count - 1) } else { viewModel.needLoadMiddleIndex = nil } - viewModel.hashtagStatusIDList.insert(contentsOf: newStatusIDList, at: 0) - viewModel.hashtagStatusIDList.removeDuplicates() + oldStatusIDs.insert(contentsOf: newStatusIDList, at: 0) + let newIDs = oldStatusIDs.removingDuplicates() - let newPredicate = Status.predicate(domain: activeMastodonAuthenticationBox.domain, ids: viewModel.hashtagStatusIDList) - viewModel.timelinePredicate.send(newPredicate) + viewModel.fetchedResultsController.statusIDs.value = newIDs } .store(in: &viewModel.disposeBag) } diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadMiddleState.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadMiddleState.swift index e971659e1..9bf87554b 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadMiddleState.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadMiddleState.swift @@ -54,11 +54,11 @@ extension HashtagTimelineViewModel.LoadMiddleState { return } - guard let upperStatusObject = (viewModel.fetchedResultsController.fetchedObjects ?? []).first(where: { $0.objectID == upperStatusObjectID }) else { + guard let upperStatusObject = (viewModel.fetchedResultsController.fetchedResultsController.fetchedObjects ?? []).first(where: { $0.objectID == upperStatusObjectID }) else { stateMachine.enter(Fail.self) return } - let statusIDs = (viewModel.fetchedResultsController.fetchedObjects ?? []).compactMap { status in + let statusIDs = (viewModel.fetchedResultsController.fetchedResultsController.fetchedObjects ?? []).compactMap { status in status.id } @@ -86,27 +86,27 @@ extension HashtagTimelineViewModel.LoadMiddleState { let newStatusIDList = response.value.map { $0.id } - if let indexToInsert = viewModel.hashtagStatusIDList.firstIndex(of: maxID) { + var oldStatusIDs = viewModel.fetchedResultsController.statusIDs.value + if let indexToInsert = oldStatusIDs.firstIndex(of: maxID) { // When response data: // 1. is not empty // 2. last status are not recorded // Then we may have middle data to load if let lastNewStatusID = newStatusIDList.last, - !viewModel.hashtagStatusIDList.contains(lastNewStatusID) { + !oldStatusIDs.contains(lastNewStatusID) { viewModel.needLoadMiddleIndex = indexToInsert + newStatusIDList.count } else { viewModel.needLoadMiddleIndex = nil } - viewModel.hashtagStatusIDList.insert(contentsOf: newStatusIDList, at: indexToInsert + 1) - viewModel.hashtagStatusIDList.removeDuplicates() + oldStatusIDs.insert(contentsOf: newStatusIDList, at: indexToInsert + 1) + oldStatusIDs.removeDuplicates() } else { // Only when the hashtagStatusIDList changes, we could not find the `loadMiddleState` index // Then there is no need to set a `loadMiddleState` cell viewModel.needLoadMiddleIndex = nil } - let newPredicate = Status.predicate(domain: activeMastodonAuthenticationBox.domain, ids: viewModel.hashtagStatusIDList) - viewModel.timelinePredicate.send(newPredicate) + viewModel.fetchedResultsController.statusIDs.value = oldStatusIDs } .store(in: &viewModel.disposeBag) diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadOldestState.swift index d464d3a50..23ec99152 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadOldestState.swift @@ -29,7 +29,7 @@ extension HashtagTimelineViewModel.LoadOldestState { class Initial: HashtagTimelineViewModel.LoadOldestState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { guard let viewModel = viewModel else { return false } - guard !(viewModel.fetchedResultsController.fetchedObjects ?? []).isEmpty else { return false } + guard !(viewModel.fetchedResultsController.fetchedResultsController.fetchedObjects ?? []).isEmpty else { return false } return stateClass == Loading.self } } @@ -48,7 +48,7 @@ extension HashtagTimelineViewModel.LoadOldestState { return } - guard let last = viewModel.fetchedResultsController.fetchedObjects?.last else { + guard let last = viewModel.fetchedResultsController.fetchedResultsController.fetchedObjects?.last else { stateMachine.enter(Idle.self) return } @@ -79,10 +79,10 @@ extension HashtagTimelineViewModel.LoadOldestState { } else { stateMachine.enter(Idle.self) } - let newStatusIDList = statuses.map { $0.id } - viewModel.hashtagStatusIDList.append(contentsOf: newStatusIDList) - let newPredicate = Status.predicate(domain: activeMastodonAuthenticationBox.domain, ids: viewModel.hashtagStatusIDList) - viewModel.timelinePredicate.send(newPredicate) + var newStatusIDs = viewModel.fetchedResultsController.statusIDs.value + let fetchedStatusIDList = statuses.map { $0.id } + newStatusIDs.append(contentsOf: fetchedStatusIDList) + viewModel.fetchedResultsController.statusIDs.value = newStatusIDs } .store(in: &viewModel.disposeBag) } diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift index e7f167f2a..a6b1b0594 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift @@ -19,12 +19,11 @@ final class HashtagTimelineViewModel: NSObject { var disposeBag = Set() - var hashtagStatusIDList = [Mastodon.Entity.Status.ID]() var needLoadMiddleIndex: Int? = nil // input let context: AppContext - let fetchedResultsController: NSFetchedResultsController + let fetchedResultsController: StatusFetchedResultsController let isFetchingLatestTimeline = CurrentValueSubject(false) let timelinePredicate = CurrentValueSubject(nil) let hashtagEntity = CurrentValueSubject(nil) @@ -69,39 +68,14 @@ final class HashtagTimelineViewModel: NSObject { init(context: AppContext, hashTag: String) { self.context = context self.hashTag = hashTag - self.fetchedResultsController = { - let fetchRequest = Status.sortedFetchRequest - fetchRequest.returnsObjectsAsFaults = false - fetchRequest.fetchBatchSize = 20 - let controller = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: context.managedObjectContext, - sectionNameKeyPath: nil, - cacheName: nil - ) - - return controller - }() + let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value + self.fetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: activeMastodonAuthenticationBox?.domain, additionalTweetPredicate: nil) super.init() - fetchedResultsController.delegate = self - - timelinePredicate + fetchedResultsController.objectIDs .receive(on: DispatchQueue.main) - .compactMap { $0 } - .sink { [weak self] predicate in - guard let self = self else { return } - self.fetchedResultsController.fetchRequest.predicate = predicate - do { - self.diffableDataSource?.defaultRowAnimation = .fade - try self.fetchedResultsController.performFetch() - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in - guard let self = self else { return } - self.diffableDataSource?.defaultRowAnimation = .automatic - } - } catch { - assertionFailure(error.localizedDescription) - } + .sink { [weak self] objectIds in + self?.generateStatusItems(newObjectIDs: objectIds) } .store(in: &disposeBag) }