Don't use NSFetchedResultsController for items list
This commit is contained in:
parent
ed0a2f1ba3
commit
e242510c5e
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: fervorController.persistentContainer.viewContext)
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
let alert = UIAlertController(title: "Error fetching items", message: error.localizedDescription, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, NSManagedObjectID> {
|
||||||
|
let itemCell = UICollectionView.CellRegistration<ItemCollectionViewCell, NSManagedObjectID> { [unowned self] cell, indexPath, itemIdentifier in
|
||||||
extension ItemsViewController: UICollectionViewDataSource {
|
|
||||||
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.delegate = self
|
||||||
cell.scrollView = collectionView
|
cell.scrollView = self.collectionView
|
||||||
cell.updateUI(item: resultsController.fetchedObjects![indexPath.row])
|
let item = self.fervorController.persistentContainer.viewContext.object(with: itemIdentifier) as! Item
|
||||||
return cell
|
cell.updateUI(item: item)
|
||||||
}
|
}
|
||||||
|
return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: itemCell, for: indexPath, item: itemIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func managedObjectsDidChange(_ notification: Notification) {
|
||||||
|
let inserted = notification.userInfo?[NSInsertedObjectsKey] as? NSSet
|
||||||
|
let updated = notification.userInfo?[NSUpdatedObjectsKey] as? NSSet
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: this feels inefficient
|
||||||
|
for (inserted, insertedPublished) in insertedItems {
|
||||||
|
// 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: NSFetchedResultsControllerDelegate {
|
extension ItemsViewController {
|
||||||
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
enum Section: Hashable {
|
||||||
batchUpdates = []
|
case items
|
||||||
}
|
|
||||||
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 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
batchUpdates.append {
|
|
||||||
switch type {
|
|
||||||
case .insert:
|
|
||||||
self.collectionView.insertItems(at: [indexPath])
|
|
||||||
case .delete:
|
|
||||||
self.collectionView.deleteItems(at: [indexPath])
|
|
||||||
case .move:
|
|
||||||
if let newIndexPath = newIndexPath {
|
|
||||||
self.collectionView.moveItem(at: indexPath, to: newIndexPath)
|
|
||||||
}
|
|
||||||
case .update:
|
|
||||||
self.collectionView.reloadItems(at: [indexPath])
|
|
||||||
@unknown default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
Loading…
Reference in New Issue