Core Data With NSFetchedResultsController and Swift

Implement the NSFetchedResultsControllerDelegate Protocol With Swift

Core Data With NSFetchedResultsController and Swift

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.

Creating a UIViewController Subclass

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.

Adding a View Controller Scene

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.

Creating a Segue

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.

Creating the User Interface of the Add Quote View Controller Scene

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.

Next Episode "Respond to Updates Using the NSFetchedResultsControllerDelegate Protocol"

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By