fix: handle status delete UI updater in thread scene

This commit is contained in:
CMK 2021-06-21 16:38:59 +08:00
parent a9cce7b3e3
commit 2dfd6168a9
4 changed files with 125 additions and 26 deletions

View File

@ -12,7 +12,7 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>17</integer>
<integer>16</integer>
</dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict>
@ -32,7 +32,7 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>16</integer>
<integer>17</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -47,14 +47,18 @@ extension StatusSection {
// configure cell
managedObjectContext.performAndWait {
let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
let timelineIndex = managedObjectContext.object(with: objectID) as? HomeTimelineIndex
// note: force check optional for status
// status maybe <uninitialized> here when delete in thread scene
guard let status = timelineIndex?.status,
let userID = timelineIndex?.userID else { return }
StatusSection.configure(
cell: cell,
dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
timestampUpdatePublisher: timestampUpdatePublisher,
status: timelineIndex.status,
requestUserID: timelineIndex.userID,
status: status,
requestUserID: userID,
statusItemAttribute: attribute
)
}
@ -752,12 +756,13 @@ extension StatusSection {
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.favouritesCount.intValue)
}()
Publishers.CombineLatest(
dependency.context.blockDomainService.blockedDomains,
dependency.context.blockDomainService.blockedDomains.setFailureType(to: ManagedObjectObserver.Error.self),
ManagedObjectObserver.observe(object: status.authorForUserProvider)
.assertNoFailure()
)
)
.receive(on: RunLoop.main)
.sink { [weak dependency, weak cell] _, change in
.sink(receiveCompletion: { _ in
// do nothing
}, receiveValue: { [weak dependency, weak cell] _, change in
guard let cell = cell else { return }
guard let dependency = dependency else { return }
switch change.changeType {
@ -769,7 +774,7 @@ extension StatusSection {
break
}
StatusSection.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
}
})
.store(in: &cell.disposeBag)
self.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
}

View File

@ -8,6 +8,8 @@
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
extension ThreadViewModel {
@ -41,13 +43,29 @@ extension ThreadViewModel {
diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
Publishers.CombineLatest3(
rootItem.removeDuplicates(),
ancestorItems.removeDuplicates(),
descendantItems.removeDuplicates()
)
.receive(on: RunLoop.main)
.sink { [weak self] rootItem, ancestorItems, descendantItems in
guard let self = self else { return }
var items: [Item] = []
rootItem.flatMap { items.append($0) }
items.append(contentsOf: ancestorItems)
items.append(contentsOf: descendantItems)
self.updateDeletedStatus(for: items)
}
.store(in: &disposeBag)
Publishers.CombineLatest4(
rootItem,
ancestorItems,
descendantItems
descendantItems,
existStatusFetchedResultsController.objectIDs
)
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) // some magic to avoid jitter
.receive(on: DispatchQueue.main)
.sink { [weak self] rootItem, ancestorItems, descendantItems in
.debounce(for: .milliseconds(100), scheduler: RunLoop.main) // some magic to avoid jitter
.sink { [weak self] rootItem, ancestorItems, descendantItems, existObjectIDs in
guard let self = self else { return }
guard let tableView = self.tableView,
let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar()
@ -65,31 +83,42 @@ extension ThreadViewModel {
if self.rootNode.value?.replyToID != nil, !(currentState is LoadThreadState.NoMore) {
newSnapshot.appendItems([.topLoader], toSection: .main)
}
let ancestorItems = ancestorItems.filter { item in
guard case let .reply(statusObjectID, _) = item else { return false }
return existObjectIDs.contains(statusObjectID)
}
newSnapshot.appendItems(ancestorItems, toSection: .main)
// root
if let rootItem = rootItem {
switch rootItem {
case .root:
newSnapshot.appendItems([rootItem], toSection: .main)
default:
break
}
if let rootItem = rootItem,
case let .root(objectID, _) = rootItem,
existObjectIDs.contains(objectID) {
newSnapshot.appendItems([rootItem], toSection: .main)
}
// leaf
if !(currentState is LoadThreadState.NoMore) {
newSnapshot.appendItems([.bottomLoader], toSection: .main)
}
let descendantItems = descendantItems.filter { item in
switch item {
case .leaf(let statusObjectID, _):
return existObjectIDs.contains(statusObjectID)
default:
return true
}
}
newSnapshot.appendItems(descendantItems, toSection: .main)
// difference for first visiable item exclude .topLoader
// difference for first visible item exclude .topLoader
guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else {
diffableDataSource.apply(newSnapshot)
return
}
// addtional margin for .topLoader
// additional margin for .topLoader
let oldTopMargin: CGFloat = {
let marginHeight = TimelineTopLoaderTableViewCell.cellHeight
if oldSnapshot.itemIdentifiers.contains(.topLoader) {
@ -184,3 +213,33 @@ extension ThreadViewModel {
)
}
}
extension ThreadViewModel {
private func updateDeletedStatus(for items: [Item]) {
let parentManagedObjectContext = context.managedObjectContext
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
managedObjectContext.parent = parentManagedObjectContext
managedObjectContext.perform {
var statusIDs: [Status.ID] = []
for item in items {
switch item {
case .root(let objectID, _):
guard let status = managedObjectContext.object(with: objectID) as? Status else { continue }
statusIDs.append(status.id)
case .reply(let objectID, _):
guard let status = managedObjectContext.object(with: objectID) as? Status else { continue }
statusIDs.append(status.id)
case .leaf(let objectID, _):
guard let status = managedObjectContext.object(with: objectID) as? Status else { continue }
statusIDs.append(status.id)
default:
continue
}
}
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.existStatusFetchedResultsController.statusIDs.value = statusIDs
}
}
}
}

View File

@ -16,12 +16,14 @@ import MastodonSDK
class ThreadViewModel {
var disposeBag = Set<AnyCancellable>()
var rootItemObserver: AnyCancellable?
// input
let context: AppContext
let rootNode: CurrentValueSubject<RootNode?, Never>
let rootItem: CurrentValueSubject<Item?, Never>
let cellFrameCache = NSCache<NSNumber, NSValue>()
let existStatusFetchedResultsController: StatusFetchedResultsController
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
weak var tableView: UITableView?
@ -49,10 +51,20 @@ class ThreadViewModel {
self.context = context
self.rootNode = CurrentValueSubject(optionalStatus.flatMap { RootNode(domain: $0.domain, statusID: $0.id, replyToID: $0.inReplyToID) })
self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) })
self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil)
self.navigationBarTitle = CurrentValueSubject(
optionalStatus.flatMap { L10n.Scene.Thread.title($0.author.displayNameWithFallback) }
)
// bind fetcher domain
context.authenticationService.activeMastodonAuthenticationBox
.receive(on: RunLoop.main)
.sink { [weak self] box in
guard let self = self else { return }
self.existStatusFetchedResultsController.domain.value = box?.domain
}
.store(in: &disposeBag)
rootNode
.receive(on: DispatchQueue.main)
.sink { [weak self] rootNode in
@ -79,8 +91,32 @@ class ThreadViewModel {
.store(in: &disposeBag)
}
// descendantNodes
rootItem
.receive(on: DispatchQueue.main)
.sink { [weak self] rootItem in
guard let self = self else { return }
guard case let .root(objectID, _) = rootItem else { return }
self.context.managedObjectContext.perform {
guard let status = self.context.managedObjectContext.object(with: objectID) as? Status else {
return
}
self.rootItemObserver = ManagedObjectObserver.observe(object: status)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { _ in
// do nothing
}, receiveValue: { [weak self] change in
guard let self = self else { return }
switch change.changeType {
case .delete:
self.rootItem.value = nil
default:
break
}
})
}
}
.store(in: &disposeBag)
ancestorNodes
.receive(on: DispatchQueue.main)
.compactMap { [weak self] nodes -> [Item]? in
@ -276,4 +312,3 @@ extension ThreadViewModel {
}
}