Fix crash when opening profile view controller with uncached account

E.g., by tapping a mention in a status
This commit is contained in:
Shadowfacts 2022-10-29 18:55:13 -04:00
parent cbbe9ec11f
commit d5433e9b91
3 changed files with 45 additions and 19 deletions

View File

@ -147,19 +147,20 @@ class MastodonCachePersistentStore: NSPersistentContainer {
} }
@discardableResult @discardableResult
private func upsert(account: Account) -> AccountMO { private func upsert(account: Account, in context: NSManagedObjectContext) -> AccountMO {
if let accountMO = self.account(for: account.id, in: self.backgroundContext) { if let accountMO = self.account(for: account.id, in: context) {
accountMO.updateFrom(apiAccount: account, container: self) accountMO.updateFrom(apiAccount: account, container: self)
return accountMO return accountMO
} else { } else {
return AccountMO(apiAccount: account, container: self, context: self.backgroundContext) return AccountMO(apiAccount: account, container: self, context: context)
} }
} }
func addOrUpdate(account: Account, completion: ((AccountMO) -> Void)? = nil) { func addOrUpdate(account: Account, in context: NSManagedObjectContext? = nil, completion: ((AccountMO) -> Void)? = nil) {
backgroundContext.perform { let context = context ?? backgroundContext
let accountMO = self.upsert(account: account) context.perform {
self.save(context: self.backgroundContext) let accountMO = self.upsert(account: account, in: context)
self.save(context: context)
completion?(accountMO) completion?(accountMO)
self.accountSubject.send(account.id) self.accountSubject.send(account.id)
} }
@ -199,7 +200,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
func addAll(accounts: [Account], completion: (() -> Void)? = nil) { func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
backgroundContext.perform { backgroundContext.perform {
accounts.forEach { self.upsert(account: $0) } accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
self.save(context: self.backgroundContext) self.save(context: self.backgroundContext)
completion?() completion?()
accounts.forEach { self.accountSubject.send($0.id) } accounts.forEach { self.accountSubject.send($0.id) }
@ -213,7 +214,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
// since the status has the same account as the notification // since the status has the same account as the notification
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account } let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) } statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
accounts.forEach { self.upsert(account: $0) } accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
self.save(context: self.backgroundContext) self.save(context: self.backgroundContext)
completion?() completion?()
statuses.forEach { self.statusSubject.send($0.id) } statuses.forEach { self.statusSubject.send($0.id) }
@ -227,7 +228,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
var updatedStatuses = [String]() var updatedStatuses = [String]()
block(self.backgroundContext, { (accounts) in block(self.backgroundContext, { (accounts) in
accounts.forEach { self.upsert(account: $0) } accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
updatedAccounts.append(contentsOf: accounts.map { $0.id }) updatedAccounts.append(contentsOf: accounts.map { $0.id })
}, { (statuses) in }, { (statuses) in
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) } statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }

View File

@ -29,9 +29,10 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
view as! UICollectionView view as! UICollectionView
} }
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private(set) var headerCell: ProfileHeaderCollectionViewCell? private(set) var headerCell: ProfileHeaderCollectionViewCell?
private var state: State = .unloaded
init(accountID: String?, kind: Kind, owner: ProfileViewController) { init(accountID: String?, kind: Kind, owner: ProfileViewController) {
self.accountID = accountID self.accountID = accountID
self.kind = kind self.kind = kind
@ -92,9 +93,18 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.filter { [unowned self] in $0 == self.accountID } .filter { [unowned self] in $0 == self.accountID }
.sink { [unowned self] id in .sink { [unowned self] id in
var snapshot = dataSource.snapshot() switch state {
snapshot.reconfigureItems([.header(id)]) case .unloaded:
dataSource.apply(snapshot, animatingDifferences: true) Task {
await load()
}
case .loading:
break
case .loaded, .addedHeader:
var snapshot = dataSource.snapshot()
snapshot.reconfigureItems([.header(id)])
dataSource.apply(snapshot, animatingDifferences: true)
}
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
@ -159,20 +169,26 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
} }
private func load() async { private func load() async {
guard accountID != nil, guard isViewLoaded,
await controller.state == .notLoadedInitial, let accountID,
isViewLoaded else { case .unloaded = state,
mastodonController.persistentContainer.account(for: accountID) != nil else {
return return
} }
state = .loading
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.header, .pinned, .statuses]) snapshot.appendSections([.header, .pinned, .statuses])
snapshot.appendItems([.header(accountID)], toSection: .header) snapshot.appendItems([.header(accountID)], toSection: .header)
await apply(snapshot, animatingDifferences: false) await apply(snapshot, animatingDifferences: false)
print("added header item")
state = .addedHeader
await controller.loadInitial() await controller.loadInitial()
await tryLoadPinned() await tryLoadPinned()
state = .loaded
} }
private func tryLoadPinned() async { private func tryLoadPinned() async {
@ -222,6 +238,15 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
} }
extension ProfileStatusesViewController {
enum State {
case unloaded
case loading
case addedHeader
case loaded
}
}
extension ProfileStatusesViewController { extension ProfileStatusesViewController {
enum Kind { enum Kind {
case statuses, withReplies, onlyMedia case statuses, withReplies, onlyMedia

View File

@ -116,7 +116,7 @@ class ProfileViewController: UIPageViewController {
let req = Client.getAccount(id: accountID) let req = Client.getAccount(id: accountID)
let (account, _) = try await mastodonController.run(req) let (account, _) = try await mastodonController.run(req)
let mo = await withCheckedContinuation { continuation in let mo = await withCheckedContinuation { continuation in
mastodonController.persistentContainer.addOrUpdate(account: account) { (mo) in mastodonController.persistentContainer.addOrUpdate(account: account, in: mastodonController.persistentContainer.viewContext) { (mo) in
continuation.resume(returning: mo) continuation.resume(returning: mo)
} }
} }