chore: migrate HashtagViewModel to use `StatusFetchedResultsController`

This commit is contained in:
jk234ert 2021-04-07 16:37:05 +08:00
parent a61e662f38
commit 2d65bda7fe
9 changed files with 42 additions and 161 deletions

View File

@ -8,7 +8,6 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
0F1E2D0B2615C39400C38565 /* HashtagTimelineTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F1E2D0A2615C39400C38565 /* HashtagTimelineTitleView.swift */; }; 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 */; }; 0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */; };
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202200261326E6000C64BF /* HashtagTimelineViewModel.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 */; }; 0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */; };
@ -357,7 +356,6 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
0F1E2D0A2615C39400C38565 /* HashtagTimelineTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineTitleView.swift; sourceTree = "<group>"; }; 0F1E2D0A2615C39400C38565 /* HashtagTimelineTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineTitleView.swift; sourceTree = "<group>"; };
0F1E2FE8261D7FD000C38565 /* StatusWithGapFetchResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusWithGapFetchResultController.swift; sourceTree = "<group>"; };
0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; }; 0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewModel.swift; sourceTree = "<group>"; }; 0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewModel.swift; sourceTree = "<group>"; };
0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; }; 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
@ -1661,7 +1659,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */, DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */,
0F1E2FE8261D7FD000C38565 /* StatusWithGapFetchResultController.swift */,
); );
path = FetchedResultsController; path = FetchedResultsController;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2140,7 +2137,6 @@
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */, DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */,
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */, 2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
0F1E2FE9261D7FD000C38565 /* StatusWithGapFetchResultController.swift in Sources */,
DBCC3B7B261443AD0045B23D /* ViewController.swift in Sources */, DBCC3B7B261443AD0045B23D /* ViewController.swift in Sources */,
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */, 2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */,
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */, 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,

View File

@ -25,7 +25,7 @@ final class StatusFetchedResultsController: NSObject {
// output // output
let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate) { init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) {
self.domain.value = domain ?? "" self.domain.value = domain ?? ""
self.fetchedResultsController = { self.fetchedResultsController = {
let fetchRequest = Status.sortedFetchRequest let fetchRequest = Status.sortedFetchRequest
@ -52,10 +52,11 @@ final class StatusFetchedResultsController: NSObject {
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] domain, ids in .sink { [weak self] domain, ids in
guard let self = self else { return } guard let self = self else { return }
self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ var predicates = [Status.predicate(domain: domain ?? "", ids: ids)]
Status.predicate(domain: domain ?? "", ids: ids), if let additionalPredicate = additionalTweetPredicate {
additionalTweetPredicate predicates.append(additionalPredicate)
]) }
self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
do { do {
try self.fetchedResultsController.performFetch() try self.fetchedResultsController.performFetch()
} catch { } catch {

View File

@ -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<AnyCancellable>()
let fetchedResultsController: NSFetchedResultsController<Status>
// input
let domain = CurrentValueSubject<String?, Never>(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<NSFetchRequestResult>, 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
}
}

View File

@ -108,6 +108,8 @@ extension HashtagTimelineViewController {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {

View File

@ -33,14 +33,9 @@ extension HashtagTimelineViewModel {
} }
} }
// MARK: - NSFetchedResultsControllerDelegate // MARK: - Compare old & new snapshots and generate new items
extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate { extension HashtagTimelineViewModel {
func generateStatusItems(newObjectIDs: [NSManagedObjectID]) {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let tableView = self.tableView else { return } guard let tableView = self.tableView else { return }
@ -48,12 +43,12 @@ extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate {
guard let diffableDataSource = self.diffableDataSource else { return } guard let diffableDataSource = self.diffableDataSource else { return }
let parentManagedObjectContext = fetchedResultsController.managedObjectContext let parentManagedObjectContext = fetchedResultsController.fetchedResultsController.managedObjectContext
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
managedObjectContext.parent = parentManagedObjectContext managedObjectContext.parent = parentManagedObjectContext
let oldSnapshot = diffableDataSource.snapshot() let oldSnapshot = diffableDataSource.snapshot()
let snapshot = snapshot as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID> // let snapshot = snapshot as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>
var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:] var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
for item in oldSnapshot.itemIdentifiers { for item in oldSnapshot.itemIdentifiers {
@ -61,9 +56,7 @@ extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate {
oldSnapshotAttributeDict[objectID] = attribute oldSnapshotAttributeDict[objectID] = attribute
} }
let statusItemList: [Item] = snapshot.itemIdentifiers.map { let statusItemList: [Item] = newObjectIDs.map {
let status = managedObjectContext.object(with: $0) as! Status
let attribute = oldSnapshotAttributeDict[$0] ?? Item.StatusAttribute() let attribute = oldSnapshotAttributeDict[$0] ?? Item.StatusAttribute()
return Item.status(objectID: $0, attribute: attribute) return Item.status(objectID: $0, attribute: attribute)
} }
@ -75,7 +68,7 @@ extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate {
if let needLoadMiddleIndex = needLoadMiddleIndex, needLoadMiddleIndex < (statusItemList.count - 1) { if let needLoadMiddleIndex = needLoadMiddleIndex, needLoadMiddleIndex < (statusItemList.count - 1) {
// If yes, insert a `middleLoader` at the index // If yes, insert a `middleLoader` at the index
var newItems = statusItemList 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) newSnapshot.appendItems(newItems, toSection: .main)
} else { } else {
newSnapshot.appendItems(statusItemList, toSection: .main) newSnapshot.appendItems(statusItemList, toSection: .main)
@ -112,6 +105,7 @@ extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate {
newSnapshot: NSDiffableDataSourceSnapshot<StatusSection, T> newSnapshot: NSDiffableDataSourceSnapshot<StatusSection, T>
) -> Difference<T>? { ) -> Difference<T>? {
guard oldSnapshot.numberOfItems != 0 else { return nil } 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! let oldItemAtBeginning = oldSnapshot.itemIdentifiers(inSection: .main).first!
@ -127,5 +121,4 @@ extension HashtagTimelineViewModel: NSFetchedResultsControllerDelegate {
} }
return nil return nil
} }
} }

View File

@ -73,18 +73,18 @@ extension HashtagTimelineViewModel.LoadLatestState {
// 1. is not empty // 1. is not empty
// 2. last status are not recorded // 2. last status are not recorded
// Then we may have middle data to load // Then we may have middle data to load
if !viewModel.hashtagStatusIDList.isEmpty, let lastNewStatusID = newStatusIDList.last, var oldStatusIDs = viewModel.fetchedResultsController.statusIDs.value
!viewModel.hashtagStatusIDList.contains(lastNewStatusID) { if !oldStatusIDs.isEmpty, let lastNewStatusID = newStatusIDList.last,
!oldStatusIDs.contains(lastNewStatusID) {
viewModel.needLoadMiddleIndex = (newStatusIDList.count - 1) viewModel.needLoadMiddleIndex = (newStatusIDList.count - 1)
} else { } else {
viewModel.needLoadMiddleIndex = nil viewModel.needLoadMiddleIndex = nil
} }
viewModel.hashtagStatusIDList.insert(contentsOf: newStatusIDList, at: 0) oldStatusIDs.insert(contentsOf: newStatusIDList, at: 0)
viewModel.hashtagStatusIDList.removeDuplicates() let newIDs = oldStatusIDs.removingDuplicates()
let newPredicate = Status.predicate(domain: activeMastodonAuthenticationBox.domain, ids: viewModel.hashtagStatusIDList) viewModel.fetchedResultsController.statusIDs.value = newIDs
viewModel.timelinePredicate.send(newPredicate)
} }
.store(in: &viewModel.disposeBag) .store(in: &viewModel.disposeBag)
} }

View File

@ -54,11 +54,11 @@ extension HashtagTimelineViewModel.LoadMiddleState {
return 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) stateMachine.enter(Fail.self)
return return
} }
let statusIDs = (viewModel.fetchedResultsController.fetchedObjects ?? []).compactMap { status in let statusIDs = (viewModel.fetchedResultsController.fetchedResultsController.fetchedObjects ?? []).compactMap { status in
status.id status.id
} }
@ -86,27 +86,27 @@ extension HashtagTimelineViewModel.LoadMiddleState {
let newStatusIDList = response.value.map { $0.id } 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: // When response data:
// 1. is not empty // 1. is not empty
// 2. last status are not recorded // 2. last status are not recorded
// Then we may have middle data to load // Then we may have middle data to load
if let lastNewStatusID = newStatusIDList.last, if let lastNewStatusID = newStatusIDList.last,
!viewModel.hashtagStatusIDList.contains(lastNewStatusID) { !oldStatusIDs.contains(lastNewStatusID) {
viewModel.needLoadMiddleIndex = indexToInsert + newStatusIDList.count viewModel.needLoadMiddleIndex = indexToInsert + newStatusIDList.count
} else { } else {
viewModel.needLoadMiddleIndex = nil viewModel.needLoadMiddleIndex = nil
} }
viewModel.hashtagStatusIDList.insert(contentsOf: newStatusIDList, at: indexToInsert + 1) oldStatusIDs.insert(contentsOf: newStatusIDList, at: indexToInsert + 1)
viewModel.hashtagStatusIDList.removeDuplicates() oldStatusIDs.removeDuplicates()
} else { } else {
// Only when the hashtagStatusIDList changes, we could not find the `loadMiddleState` index // Only when the hashtagStatusIDList changes, we could not find the `loadMiddleState` index
// Then there is no need to set a `loadMiddleState` cell // Then there is no need to set a `loadMiddleState` cell
viewModel.needLoadMiddleIndex = nil viewModel.needLoadMiddleIndex = nil
} }
let newPredicate = Status.predicate(domain: activeMastodonAuthenticationBox.domain, ids: viewModel.hashtagStatusIDList) viewModel.fetchedResultsController.statusIDs.value = oldStatusIDs
viewModel.timelinePredicate.send(newPredicate)
} }
.store(in: &viewModel.disposeBag) .store(in: &viewModel.disposeBag)

View File

@ -29,7 +29,7 @@ extension HashtagTimelineViewModel.LoadOldestState {
class Initial: HashtagTimelineViewModel.LoadOldestState { class Initial: HashtagTimelineViewModel.LoadOldestState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool { override func isValidNextState(_ stateClass: AnyClass) -> Bool {
guard let viewModel = viewModel else { return false } 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 return stateClass == Loading.self
} }
} }
@ -48,7 +48,7 @@ extension HashtagTimelineViewModel.LoadOldestState {
return return
} }
guard let last = viewModel.fetchedResultsController.fetchedObjects?.last else { guard let last = viewModel.fetchedResultsController.fetchedResultsController.fetchedObjects?.last else {
stateMachine.enter(Idle.self) stateMachine.enter(Idle.self)
return return
} }
@ -79,10 +79,10 @@ extension HashtagTimelineViewModel.LoadOldestState {
} else { } else {
stateMachine.enter(Idle.self) stateMachine.enter(Idle.self)
} }
let newStatusIDList = statuses.map { $0.id } var newStatusIDs = viewModel.fetchedResultsController.statusIDs.value
viewModel.hashtagStatusIDList.append(contentsOf: newStatusIDList) let fetchedStatusIDList = statuses.map { $0.id }
let newPredicate = Status.predicate(domain: activeMastodonAuthenticationBox.domain, ids: viewModel.hashtagStatusIDList) newStatusIDs.append(contentsOf: fetchedStatusIDList)
viewModel.timelinePredicate.send(newPredicate) viewModel.fetchedResultsController.statusIDs.value = newStatusIDs
} }
.store(in: &viewModel.disposeBag) .store(in: &viewModel.disposeBag)
} }

View File

@ -19,12 +19,11 @@ final class HashtagTimelineViewModel: NSObject {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
var hashtagStatusIDList = [Mastodon.Entity.Status.ID]()
var needLoadMiddleIndex: Int? = nil var needLoadMiddleIndex: Int? = nil
// input // input
let context: AppContext let context: AppContext
let fetchedResultsController: NSFetchedResultsController<Status> let fetchedResultsController: StatusFetchedResultsController
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false) let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
let timelinePredicate = CurrentValueSubject<NSPredicate?, Never>(nil) let timelinePredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
let hashtagEntity = CurrentValueSubject<Mastodon.Entity.Tag?, Never>(nil) let hashtagEntity = CurrentValueSubject<Mastodon.Entity.Tag?, Never>(nil)
@ -69,39 +68,14 @@ final class HashtagTimelineViewModel: NSObject {
init(context: AppContext, hashTag: String) { init(context: AppContext, hashTag: String) {
self.context = context self.context = context
self.hashTag = hashTag self.hashTag = hashTag
self.fetchedResultsController = { let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value
let fetchRequest = Status.sortedFetchRequest self.fetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: activeMastodonAuthenticationBox?.domain, additionalTweetPredicate: nil)
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.fetchBatchSize = 20
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: context.managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil
)
return controller
}()
super.init() super.init()
fetchedResultsController.delegate = self fetchedResultsController.objectIDs
timelinePredicate
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.compactMap { $0 } .sink { [weak self] objectIds in
.sink { [weak self] predicate in self?.generateStatusItems(newObjectIDs: objectIds)
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)
}
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }