Skip to content

Instantly share code, notes, and snippets.

@bwoods
Last active September 6, 2018 07:35
Show Gist options
  • Save bwoods/5601342 to your computer and use it in GitHub Desktop.
Save bwoods/5601342 to your computer and use it in GitHub Desktop.

NSFetchedResultsControllerDelegates

An extension that allows you to setup an NSFetchResultsConttroller as a UITableViewDataSource or UICollectionViewController in a single line of code.

self.tableView.dataSource = self.fetchedResultsController

All edits to CoreData will be reflected automatically in any relevent UITableViews or UICollectionViews.

Example Usage

class BookshelfTableViewController: UITableViewController {
    lazy var fetchedResultsController = { () -> NSFetchedResultsController<Book> in
        let request = NSFetchRequest<Book>(entityName: "Book")
        request.sortDescriptors = [ NSSortDescriptor(key: "title", ascending: true) ]

        let context = NSPersistentContainer.shared.viewContext
        let fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
        try! fetchedResultsController.performFetch()
        fetchedResultsController.delegate = self
        return fetchedResultsController
        }()


// MARK: - UITableViewDelegate methods
    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        let document = fetchedResultsController.object(at: indexPath)

        cell.textLabel!.text = document.title ?? "Untitled"
        cell.detailTextLabel!.text = document.subject
        cell.imageView!.image = document.image
    }

// MARK: - UIViewController methods
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        self.tableView.dataSource = fetchedResultsController
        fetchedResultsController.swipeToDelete = true
}
import UIKit
import CoreData
fileprivate var key: Int = 0
extension UICollectionViewController : NSFetchedResultsControllerDelegate {
fileprivate var animations: [() -> Void]! {
get { return objc_getAssociatedObject(self, &key) as? [() -> Void] }
set { objc_setAssociatedObject(self, &key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex section: Int, for type: NSFetchedResultsChangeType) {
switch(type) {
case .insert:
self.animations.append { [weak self] in
self?.collectionView?.insertSections(IndexSet(integer: section))
}
case .delete:
self.animations.append { [weak self] in
self?.collectionView?.deleteSections(IndexSet(integer: section))
}
default:
break
}
}
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch(type) {
case .insert:
if let newIndexPath = newIndexPath {
self.animations.append { [weak self] in
self?.collectionView?.insertItems(at: [ newIndexPath ])
}
}
case .delete:
if let indexPath = indexPath {
self.animations.append { [weak self] in
self?.collectionView?.deleteItems(at: [ indexPath ])
}
}
case .update:
if let indexPath = indexPath {
self.animations.append { [weak self] in
UIView.performWithoutAnimation {
self?.collectionView!.reloadItems(at: [ indexPath ])
}
}
}
case .move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
self.animations.append { [weak self] in
self?.collectionView?.deleteItems(at: [ indexPath ])
self?.collectionView?.insertItems(at: [ newIndexPath ])
}
}
}
}
public func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.animations = [() -> Void]()
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
if collectionView?.window == nil {
collectionView?.reloadData() // see: http://aplus.rs/2014/one-not-weird-trick-to-save-your-sanity-with-nsfetchedresultscontroller/
} else {
if self.animations.count <= 1 {
self.animations.first?() // remove unnecessary animations; in-place updates were causing an .insert like animation…
} else {
self.collectionView!.performBatchUpdates({
for item in self.animations {
item()
}
}, completion: { finished in
self.animations = nil
})
}
}
}
}
extension NSFetchedResultsController : UICollectionViewDataSource {
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let controller = self as! NSFetchedResultsController<NSFetchRequestResult>
return self.sections?[section].numberOfObjects ?? controller.fetchedObjects!.count
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
}
}
import UIKit
import CoreData
extension UITableViewController : NSFetchedResultsControllerDelegate {
func controller(controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex section: Int, forChangeType type: NSFetchedResultsChangeType) {
switch(type) {
case .insert:
tableView.insertSections(IndexSet(integer: section), with: .fade)
case .delete:
tableView.deleteSections(IndexSet(integer: section), with: .fade)
default:
break
}
}
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch(type) {
case .insert:
if let newIndexPath = newIndexPath {
tableView.insertRows(at: [ newIndexPath ], with: .automatic)
}
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [ indexPath ], with: .automatic)
}
case .update:
if let indexPath = indexPath {
UIView.performWithoutAnimation {
tableView.reloadRows(at: [ indexPath ], with: .none)
}
}
case .move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
tableView.deleteRows(at: [ indexPath ], with: .fade)
tableView.insertRows(at: [ newIndexPath ], with: .fade)
}
}
}
public func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
}
fileprivate var key: Int = 0
extension NSFetchedResultsController : UITableViewDataSource {
@objc var swipeToDelete: Bool {
get { return objc_getAssociatedObject(self, &key) as? Bool ?? false }
set { objc_setAssociatedObject(self, &key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
// MARK: - UITableViewDataSource editing methods
public func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return self.swipeToDelete
}
public func tableView(_ tableView: UITableView, commit edit: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if edit == .delete {
let object = self.object(at: indexPath) as! NSManagedObject
object.managedObjectContext!.delete(object)
}
}
// MARK: - UITableViewDataSource methods
public func numberOfSections(in tableView: UITableView) -> Int {
return self.sections?.count ?? 1
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let controller = self as! NSFetchedResultsController<NSFetchRequestResult>
return self.sections?[section].numberOfObjects ?? controller.fetchedObjects!.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(withIdentifier: "Cell")!
}
public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.sections?[section].name
}
public func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return self.sectionIndexTitles
}
public func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
return self.section(forSectionIndexTitle: title, at: index)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment