Don't use NSFetchedResultsController for items list

This commit is contained in:
Shadowfacts 2022-01-19 19:09:54 -05:00
parent ed0a2f1ba3
commit e242510c5e
2 changed files with 124 additions and 60 deletions

View File

@ -189,6 +189,22 @@ extension HomeViewController {
return req return req
} }
var idFetchRequest: NSFetchRequest<NSManagedObjectID> {
let req = NSFetchRequest<NSManagedObjectID>(entityName: "Item")
req.resultType = .managedObjectIDResultType
switch self {
case .unread:
req.predicate = NSPredicate(format: "read = NO")
case .all:
break
case .group(let group):
req.predicate = NSPredicate(format: "feed in %@", group.feeds!)
case .feed(let feed):
req.predicate = NSPredicate(format: "feed = %@", feed)
}
return req
}
var countFetchRequest: NSFetchRequest<Reader.Item>? { var countFetchRequest: NSFetchRequest<Reader.Item>? {
let req = Reader.Item.fetchRequest() let req = Reader.Item.fetchRequest()
switch self { switch self {
@ -229,7 +245,7 @@ extension HomeViewController: UICollectionViewDelegate {
guard let item = dataSource.itemIdentifier(for: indexPath) else { guard let item = dataSource.itemIdentifier(for: indexPath) else {
return return
} }
let vc = ItemsViewController(fetchRequest: item.fetchRequest, fervorController: fervorController) let vc = ItemsViewController(fetchRequest: item.idFetchRequest, fervorController: fervorController)
vc.title = item.title vc.title = item.title
vc.delegate = itemsDelegate vc.delegate = itemsDelegate
show(vc, sender: nil) show(vc, sender: nil)
@ -241,7 +257,7 @@ extension HomeViewController: UICollectionViewDelegate {
return nil return nil
} }
return UIContextMenuConfiguration(identifier: nil, previewProvider: { return UIContextMenuConfiguration(identifier: nil, previewProvider: {
return ItemsViewController(fetchRequest: item.fetchRequest, fervorController: self.fervorController) return ItemsViewController(fetchRequest: item.idFetchRequest, fervorController: self.fervorController)
}, actionProvider: nil) }, actionProvider: nil)
} }

View File

@ -18,14 +18,16 @@ class ItemsViewController: UIViewController {
weak var delegate: ItemsViewControllerDelegate? weak var delegate: ItemsViewControllerDelegate?
let fervorController: FervorController let fervorController: FervorController
let fetchRequest: NSFetchRequest<Item> let fetchRequest: NSFetchRequest<NSManagedObjectID>
private var collectionView: UICollectionView! private var collectionView: UICollectionView!
private var resultsController: NSFetchedResultsController<Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, NSManagedObjectID>!
private var batchUpdates: [() -> Void] = [] private var batchUpdates: [() -> Void] = []
init(fetchRequest: NSFetchRequest<Item>, fervorController: FervorController) { init(fetchRequest: NSFetchRequest<NSManagedObjectID>, fervorController: FervorController) {
precondition(fetchRequest.entityName == "Item")
self.fervorController = fervorController self.fervorController = fervorController
self.fetchRequest = fetchRequest self.fetchRequest = fetchRequest
@ -47,12 +49,11 @@ class ItemsViewController: UIViewController {
configuration.backgroundColor = .clear configuration.backgroundColor = .clear
let layout = UICollectionViewCompositionalLayout.list(using: configuration) let layout = UICollectionViewCompositionalLayout.list(using: configuration)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(ItemCollectionViewCell.self, forCellWithReuseIdentifier: "itemCell")
view.addSubview(collectionView) view.addSubview(collectionView)
dataSource = createDataSource()
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
@ -61,69 +62,116 @@ class ItemsViewController: UIViewController {
]) ])
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)] fetchRequest.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
fetchRequest.fetchBatchSize = 20 do {
resultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: fervorController.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil) let ids = try fervorController.persistentContainer.viewContext.fetch(fetchRequest)
resultsController.delegate = self
try! resultsController.performFetch()
}
} var snapshot = NSDiffableDataSourceSnapshot<Section, NSManagedObjectID>()
snapshot.appendSections([.items])
snapshot.appendItems(ids, toSection: .items)
dataSource.apply(snapshot, animatingDifferences: false)
extension ItemsViewController: UICollectionViewDataSource { NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: fervorController.persistentContainer.viewContext)
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return resultsController.fetchedObjects?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "itemCell", for: indexPath) as! ItemCollectionViewCell
cell.delegate = self
cell.scrollView = collectionView
cell.updateUI(item: resultsController.fetchedObjects![indexPath.row])
return cell
}
}
extension ItemsViewController: NSFetchedResultsControllerDelegate { } catch {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { let alert = UIAlertController(title: "Error fetching items", message: error.localizedDescription, preferredStyle: .alert)
batchUpdates = [] alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
} present(alert, animated: true)
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView.performBatchUpdates {
for update in self.batchUpdates {
update()
}
} }
// clear to prevent retain cycles
batchUpdates = []
} }
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
guard let indexPath = indexPath else { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, NSManagedObjectID> {
return let itemCell = UICollectionView.CellRegistration<ItemCollectionViewCell, NSManagedObjectID> { [unowned self] cell, indexPath, itemIdentifier in
cell.delegate = self
cell.scrollView = self.collectionView
let item = self.fervorController.persistentContainer.viewContext.object(with: itemIdentifier) as! Item
cell.updateUI(item: item)
} }
batchUpdates.append { return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
switch type { return collectionView.dequeueConfiguredReusableCell(using: itemCell, for: indexPath, item: itemIdentifier)
case .insert: }
self.collectionView.insertItems(at: [indexPath]) }
case .delete:
self.collectionView.deleteItems(at: [indexPath]) @objc private func managedObjectsDidChange(_ notification: Notification) {
case .move: let inserted = notification.userInfo?[NSInsertedObjectsKey] as? NSSet
if let newIndexPath = newIndexPath { let updated = notification.userInfo?[NSUpdatedObjectsKey] as? NSSet
self.collectionView.moveItem(at: indexPath, to: newIndexPath) let deleted = notification.userInfo?[NSDeletedObjectsKey] as? NSSet
// managed objectss from the notification are tied to the thread it was delivered on
// so get the published dates and evaluate the predicate here
let insertedItems = inserted?.compactMap { $0 as? Item }.filter { fetchRequest.predicate?.evaluate(with: $0) ?? true }.map { ($0, $0.published) }
var snapshot = self.dataSource.snapshot()
if let updated = updated {
snapshot.reconfigureItems(updated.compactMap { ($0 as? Item)?.objectID })
}
if let deleted = deleted {
snapshot.deleteItems(deleted.compactMap { ($0 as? Item)?.objectID })
}
if let insertedItems = insertedItems {
self.fervorController.persistentContainer.performBackgroundTask { ctx in
// for newly inserted items, the ctx doesn't have the published date so we check the data we got from the notification
func publishedForItem(_ id: NSManagedObjectID) -> Date? {
if let (_, pub) = insertedItems.first(where: { $0.0.objectID == id }) {
return pub
} else {
return (ctx.object(with: id) as! Item).published
}
} }
case .update:
self.collectionView.reloadItems(at: [indexPath]) // todo: this feels inefficient
@unknown default: for (inserted, insertedPublished) in insertedItems {
break // todo: uhh, what does sql do if the published date is nil?
guard let insertedPublished = insertedPublished else {
snapshot.insertItems([inserted.objectID], beforeItem: snapshot.itemIdentifiers.first!)
continue
}
var index = 0
while index < snapshot.itemIdentifiers.count,
let pub = publishedForItem(snapshot.itemIdentifiers[index]),
insertedPublished < pub {
index += 1
}
// index is the index of the first item which the inserted one was published after
// (i.e., the item that should appear immediately after inserted in the list)
if index == snapshot.itemIdentifiers.count {
snapshot.appendItems([inserted.objectID], toSection: .items)
} else {
snapshot.insertItems([inserted.objectID], beforeItem: snapshot.itemIdentifiers[index])
}
}
DispatchQueue.main.async {
self.dataSource.apply(snapshot, animatingDifferences: true)
}
}
} else {
DispatchQueue.main.async {
self.dataSource.apply(snapshot, animatingDifferences: true)
} }
} }
} }
}
extension ItemsViewController {
enum Section: Hashable {
case items
}
} }
extension ItemsViewController: UICollectionViewDelegate { extension ItemsViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
let item = resultsController.fetchedObjects![indexPath.row] guard let id = dataSource.itemIdentifier(for: indexPath) else {
return nil
}
let item = fervorController.persistentContainer.viewContext.object(with: id) as! Item
// let item = resultsController.fetchedObjects![indexPath.row]
return UIContextMenuConfiguration(identifier: nil, previewProvider: { return UIContextMenuConfiguration(identifier: nil, previewProvider: {
ReadViewController(item: item, fervorController: self.fervorController) ReadViewController(item: item, fervorController: self.fervorController)
}, actionProvider: { _ in }, actionProvider: { _ in