In yesterday's tutorial, we populated a table view with quotes using the NSFetchedResultsController
class. But the table view is currently empty since we haven't added the ability to add quotes yet.
This tutorial focuses on the implementation of the NSFetchedResultsControllerDelegate
protocol. It allows the application to respond to changes that take place in the managed object context it observes. The fetched results controller notifies its delegate when the data it manages is modified. If we correctly implement the protocol, updating the table view after a change is a breeze.
If you want to follow along, download the source files of this tutorial at the bottom of the tutorial.
Adding Quotes
Create a UIViewController
subclass and name it AddQuoteViewController
.
Declare two outlets, one for a text field and one for a text view, and one action, save(sender:)
. We implement the action later in the tutorial.
import UIKit
class AddQuoteViewController: UIViewController {
// MARK: - Properties
@IBOutlet var authorTextField: UITextField!
@IBOutlet var contentsTextView: UITextView!
// MARK: - Actions
@IBAction func save(sender: UIBarButtonItem) {}
}
Open Main.storyboard, add a view controller to the canvas, and set the class of the view controller to AddQuoteViewController in the Identity Inspector.
Add a bar button item to the navigation bar of the view controller of the View Controller Scene and set System Item to Add in the Attributes Inspector.
Press Control and drag from the bar button item to the Add Quote View Controller Scene to create a segue. Choose Show from the section Action Segues. Select the segue and, in the Attributes Inspector, set Identifier to SegueAddQuoteViewController.
Revisit the Add Quote View Controller Scene and add a text field and a text view to the view controller of the scene. Add the necessary constraints and connect the outlets we created a moment ago.
Add a bar button item to the navigation bar, set its System Item attribute to Save, and connect it to the save(sender:) action we created earlier.
Revisit AddQuoteViewController.swift, add an import statement for the Core Data framework, and declare a property, managedObjectContext
, of type NSManagedObjectContext?
.
import UIKit
import CoreData
class AddQuoteViewController: UIViewController {
// MARK: - Properties
@IBOutlet var authorTextField: UITextField!
@IBOutlet var contentsTextView: UITextView!
// MARK: -
var managedObjectContext: NSManagedObjectContext?
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
title = "Add Quote"
}
// MARK: - Actions
@IBAction func save(sender: UIBarButtonItem) {}
}
We are almost ready to start adding quotes. Open ViewController.swift and implement prepare(for:sender:)
as shown below. We need to pass a reference of the view managed object context of the persistent container to the add quote view controller.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == segueAddQuoteViewController {
if let destinationViewController = segue.destination as? AddQuoteViewController {
// Configure View Controller
destinationViewController.managedObjectContext = persistentContainer.viewContext
}
}
}
The segueAddQuoteViewController
constant is a private property of the ViewController
class.
import UIKit
import CoreData
class ViewController: UIViewController {
// MARK: - Properties
private let segueAddQuoteViewController = "SegueAddQuoteViewController"
...
}
Implementing the NSFetchedResultsControllerDelegate Protocol
Even though we can now add quotes, the table view isn't automagically updated when a quote is added. The NSFetchedResultsController
instance needs to inform the view controller about such an event. It does this through the NSFetchedResultsControllerDelegate
protocol.
The first two methods we are interested in are:
controllerWillChangeContent(_:)
controllerDidChangeContent(_:)
As their names imply, these methods are invoked before and after the data the fetched results controller manages changes. The implementation is very easy as you can see below.
extension ViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
updateView()
}
}
It is possible that multiple changes take place in a short timeframe. We don't want to continuously update the table view, which is why we invoke beginUpdates()
when the fetched results controller tells us it is about to make changes and endUpdates()
when we are sure no other changes are going to take place. This is an optimization that pays off for applications with a more complex data model.
Notice that we also invoke updateView()
in controllerDidChangeContent(_:)
to update the user interface. This means we need to mark the updateView()
method as fileprivate
instead of private
.
fileprivate func updateView() {
var hasQuotes = false
if let quotes = fetchedResultsController.fetchedObjects {
hasQuotes = quotes.count > 0
}
tableView.isHidden = !hasQuotes
messageLabel.isHidden = hasQuotes
activityIndicatorView.stopAnimating()
}
The most important method we need to implement is controller(_:didChange:at:for:newIndexPath:)
. This method is invoked for every managed object the fetched results controller manages that is inserted, updated, or deleted. This method is invoked repeatedly if you are dealing with a complex data model that involves several entities and relationships.
The method defines five parameters:
- the
NSFetchedResultsController
instance - the managed object that was inserted, updated, or deleted
- the current index path of the managed object
- the type of change (insert, update, move, or delete)
- the new index path of the managed object
While this is a lot of information to digest, it is exactly the information we need to update the table view. We don't need to perform any additional calculations to insert, update, or delete rows in the table view.
Let me show you what the implementation looks like to support the insertion of new quotes into the table view.
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch (type) {
case .insert:
if let indexPath = newIndexPath {
tableView.insertRows(at: [indexPath], with: .fade)
}
break;
default:
print("...")
}
}
That's it. We inspect the value of the type
parameter, the type of change that took place. If we are dealing with an insertion of a managed object, we safely unwrap the value of newIndexPath
and insert a row at that index path.
Before we can test the implementation, we need to implement the save(sender:)
action of the AddQuoteViewController
class. This is straightforward as you can see below.
@IBAction func save(sender: UIBarButtonItem) {
guard let managedObjectContext = managedObjectContext else { return }
// Create Quote
let quote = Quote(context: managedObjectContext)
// Configure Quote
quote.author = authorTextField.text
quote.contents = contentsTextView.text
quote.createdAt = Date().timeIntervalSince1970
// Pop View Controller
_ = navigationController?.popViewController(animated: true)
}
If we have a managed object context to work with, we create a Quote
instance, populate it, and pop the view controller from the navigation stack.
The application should now have the ability to add quotes. Run the application and give it a try.
Deleting Quotes
Deleting quotes is very easy to implement thanks to the work we have already done. We first need to implement the tableView(_:commit:forRowAt:)
method of the UITableViewDataSource
protocol.
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Fetch Quote
let quote = fetchedResultsController.object(at: indexPath)
// Delete Quote
quote.managedObjectContext?.delete(quote)
}
}
We fetch the quote that corresponds with the value of indexPath
and we delete it from the managed object context it belongs to.
To reflect this change in the table view, we need to update the controller(_:didChange:at:for:newIndexPath:)
method. But that is trivial. Look at the updated implementation below.
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch (type) {
case .insert:
if let indexPath = newIndexPath {
tableView.insertRows(at: [indexPath], with: .fade)
}
break;
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .fade)
}
break;
default:
print("...")
}
}
Persisting Data
At the moment, the application doesn't push its changes to the persistent store. To resolve this issue, we tell the view managed object context of the persistent container to save its changes when the application is pushed to the background. In viewDidLoad()
, we add the view controller as an observer of the `` notification.
override func viewDidLoad() {
super.viewDidLoad()
...
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(_:)), name: Notification.Name.UIApplicationDidEnterBackground, object: nil)
}
In the applicationDidEnterBackground(_:)
method, we tell the view managed object context of the persistent container to save its changes.
// MARK: - Notification Handling
@objc func applicationDidEnterBackground(_ notification: Notification) {
do {
try persistentContainer.viewContext.save()
} catch {
print("Unable to Save Changes")
print("\(error), \(error.localizedDescription)")
}
}
Updating Quotes
In the next tutorial, I show you how to add support for updating quotes. Even though this is a bit more complicated, the NSFetchedResultsController
class does most of the heavy lifting for us.