Displaying data from Core Data is a lot like implementing a table view with a fix set of simple objects. The key difference is that a FetchResultsController
can answer all the questions that the data source needs answers to, usually posed when implementing the UITableViewDataSource
.
The only question (protocol) that FetchResultsController
can't answer is tableView(tableView:, cellForRowAt indexPath:)
. The reason for this is that it can't know what kind of cell we want to use.
class CoreDataTableViewController: UITableViewController {
// MARK: Properties
var fetchedResultsController : NSFetchedResultsController<NSFetchRequestResult>? {
didSet {
// Whenever the frc changes, we execute the search and
// reload the table
fetchedResultsController?.delegate = self
executeSearch()
tableView.reloadData()
}
}
// MARK: Initializers
init(fetchedResultsController fc : NSFetchedResultsController<NSFetchRequestResult>, style : UITableViewStyle = .plain) {
fetchedResultsController = fc
super.init(style: style)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
// MARK: - CoreDataTableViewController (Subclass Must Implement)
extension CoreDataTableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
fatalError("This method MUST be implemented by a subclass of CoreDataTableViewController")
}
}
// MARK: - CoreDataTableViewController (Table Data Source)
extension CoreDataTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
if let fc = fetchedResultsController {
return (fc.sections?.count)!
} else {
return 0
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let fc = fetchedResultsController {
return fc.sections![section].numberOfObjects
} else {
return 0
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if let fc = fetchedResultsController {
return fc.sections![section].name
} else {
return nil
}
}
override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
if let fc = fetchedResultsController {
return fc.section(forSectionIndexTitle: title, at: index)
} else {
return 0
}
}
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
if let fc = fetchedResultsController {
return fc.sectionIndexTitles
} else {
return nil
}
}
}
// MARK: - CoreDataTableViewController (Fetches)
extension CoreDataTableViewController {
func executeSearch() {
if let fc = fetchedResultsController {
do {
try fc.performFetch()
} catch let e as NSError {
print("Error while trying to perform a search: \n\(e)\n\(fetchedResultsController)")
}
}
}
}
// MARK: - CoreDataTableViewController: NSFetchedResultsControllerDelegate
extension CoreDataTableViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
let set = IndexSet(integer: sectionIndex)
switch (type) {
case .insert:
tableView.insertSections(set, with: .fade)
case .delete:
tableView.deleteSections(set, with: .fade)
default:
// irrelevant in our case
break
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch(type) {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .fade)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .fade)
case .update:
tableView.reloadRows(at: [indexPath!], with: .fade)
case .move:
tableView.deleteRows(at: [indexPath!], with: .fade)
tableView.insertRows(at: [newIndexPath!], with: .fade)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
}
Example - A List Of Notebooks
In this example we are going to create a new subclass of CoreDataTableViewController
.
We implement tableView(cellForRowAt: indexPath:)
and in it we to do three things.
- Find the notebook we need
- Create a cell
- Display the notebook information in the cell
To find the correct notebook we will use the NSFetchResults
method object(at:)
but before we can make use of this method we create the NSFetchResultsController
in the viewDidLoad
method. To do that we first need instantiate two things.
- An initialized stack
- An
NSFetchRequest
class NotebooksViewController: CoreDataTableViewController {
// MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Set the title
title = "CoolNotes"
// Get the stack
let delegate = UIApplication.shared.delegate as! AppDelegate
let stack = delegate.stack
// Create a fetchrequest
let fr = NSFetchRequest<NSFetchRequestResult>(entityName: "Notebook")
fr.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true),
NSSortDescriptor(key: "creationDate", ascending: false)]
// Create the FetchedResultsController
fetchedResultsController = NSFetchedResultsController(fetchRequest: fr, managedObjectContext: stack.context, sectionNameKeyPath: nil, cacheName: nil)
}
// MARK: TableView Data Source
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// This method must be implemented by our subclass. There's no way
// CoreDataTableViewController can know what type of cell we want to
// use.
// Find the right notebook for this indexpath
let nb = fetchedResultsController!.object(at: indexPath) as! Notebook
// Create the cell
let cell = tableView.dequeueReusableCell(withIdentifier: "NotebookCell", for: indexPath)
// Sync notebook -> cell
cell.textLabel?.text = nb.name
cell.detailTextLabel?.text = String(format: "%d notes", nb.notes!.count)
return cell
}
}