chore: migrate HashtagViewModel to use `StatusFetchedResultsController`
This commit is contained in:
parent
a61e662f38
commit
2d65bda7fe
|
@ -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 */,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -108,6 +108,8 @@ extension HashtagTimelineViewController {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue