Core Data With NSFetchedResultsController and Swift

Respond to Updates Using the NSFetchedResultsControllerDelegate Protocol

Core Data With NSFetchedResultsController and Swift

We can now add and remove quotes and the table view is updated accordingly. But everyone makes a mistake from time to time and we don't want to remove a quote because of a silly mistake.

In this tutorial, we add the ability to update quotes. Not only do we want the table view to reflect the changes we make to a quote, we also want to make sure the sort order of the quotes is updated when a quote is modified. You guessed it. The NSFetchedResultsController class and the NSFetchedResultsControllerDelegate protocol make this almost trivial.

If you want to follow along, download the source files of this tutorial at the bottom of the tutorial.

Adding the Ability to Edit Quotes

Because the user interface to add and update quotes is identical, we can reuse the AddQuoteViewController class. The name isn't ideal, but it works fine for this example.

Updating AddQuoteViewController

We first need to update the implementation of the AddQuoteViewController class to support editing of quotes. Open AddQuoteViewController.swift and declare a variable property, quote, of type Quote?.

import UIKit
import CoreData

class AddQuoteViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var authorTextField: UITextField!
    @IBOutlet var contentsTextView: UITextView!

    // MARK: -

    var quote: Quote?

    ...

}

We need to populate the text field and text view if the quote property has a value. We do this in the viewDidLoad() method of the AddQuoteViewController class.

override func viewDidLoad() {
    super.viewDidLoad()

    title = "Add Quote"

    if let quote = quote {
        authorTextField.text = quote.author
        contentsTextView.text = quote.contents
    }
}

The last change we need to make is updating the implementation of the save(sender:) method. If the quote property has a value, we don't instantiate a new Quote instance. Instead, we update it with the values of the text field and the text view.

@IBAction func save(sender: UIBarButtonItem) {
    guard let managedObjectContext = managedObjectContext else { return }

    if quote == nil {
        // Create Quote
        let newQuote = Quote(context: managedObjectContext)

        // Configure Quote
        newQuote.createdAt = Date().timeIntervalSince1970

        // Set Quote
        quote = newQuote
    }

    if let quote = quote {
        // Configure Quote
        quote.author = authorTextField.text
        quote.contents = contentsTextView.text
    }

    // Pop View Controller
    _ = navigationController?.popViewController(animated: true)
}

Creating a Segue

Open Main.storyboard, press Control, and drag from the prototype cell in the View Controller Scene to the view controller of the Add Quote View Controller Scene. Choose Action Segue > Show from the menu to create a segue.

Create Segue

Select the segue, open the Attributes Inspector, and set Identifier to SegueEditQuoteViewController. That's it for the user interface.

When the user taps a row, we need to fetch the quote associated with the row and pass it to the destination view controller of the segue we created in the storyboard. We do this in the prepare(for:sender:) method of the ViewController class.

We ask the fetched results controller for the managed object, a quote, that corresponds with the index path of the currently selected row. Notice that I refactored the prepare(for:sender:) method to avoid duplication.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let destinationViewController = segue.destination as? AddQuoteViewController else { return }

    // Configure View Controller
    destinationViewController.managedObjectContext = persistentContainer.viewContext

    if let indexPath = tableView.indexPathForSelectedRow, segue.identifier == segueEditQuoteViewController {
        // Configure View Controller
        destinationViewController.quote = fetchedResultsController.object(at: indexPath)
    }
}

We use another constant property for the segue identifier. I use string literals as little as possible and constant properties are great for this purpose.

import UIKit
import CoreData

class ViewController: UIViewController {

    // MARK: - Properties

    private let segueAddQuoteViewController = "SegueAddQuoteViewController"
    private let segueEditQuoteViewController = "SegueEditQuoteViewController"

    ...

}

You can now edit quotes, but the table view doesn't reflect the changes yet. We fix this in the next section.

Responding to Updates

To fix this issue, we need to update the implementation of the controller(_:didChange:at:for:newIndexPath:) method of the NSFetchedResultsControllerDelegate protocol.

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    switch (type) {
    case .insert:
        ...
    case .delete:
        ...
    case .update:
        if let indexPath = indexPath, let cell = tableView.cellForRow(at: indexPath) as? QuoteTableViewCell {
            configure(cell, at: indexPath)
        }
        break;
    default:
        print("...")
    }
}

If the change type is equal to update, we fetch the table view cell that corresponds with the value of indexPath and update it by passing it as the first argument of a helper method, configure(_:at:). Let me show you what its implementation looks like.

func configure(_ cell: QuoteTableViewCell, at indexPath: IndexPath) {
    // Fetch Quote
    let quote = fetchedResultsController.object(at: indexPath)

    // Configure Cell
    cell.authorLabel.text = quote.author
    cell.contentsLabel.text = quote.contents
}

We ask the fetched results controller for the quote that corresponds with the value of indexPath and update the QuoteTableViewCell instance. This also means that we can refactor the tableView(_:cellForRowAt:) of the UITableViewDataSource protocol.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: QuoteTableViewCell.reuseIdentifier, for: indexPath) as? QuoteTableViewCell else {
        fatalError("Unexpected Index Path")
    }

    // Configure Cell
    configure(cell, at: indexPath)

    return cell
}

Build and run the application to see the result. Any changes you make to an existing quote are immediately reflected in the table view.

Updating the Order

The table view is automagically updated when a quote is modified. But what happens if we modify a property that the fetched results controller uses to sort the quotes? Again, the NSFetchedResultsController class and the NSFetchedResultsControllerDelegate protocol come to the rescue.

To show you how this works, we need to modify the property that is used to sort the quotes displayed in the table view. Modify the sort descriptor in the implementation of the fetchedResultsController property as shown below. The quotes are now sorted based on the name of the author.

fileprivate lazy var fetchedResultsController: NSFetchedResultsController<Quote> = {
    ...

    // Configure Fetch Request
    fetchRequest.sortDescriptors = [NSSortDescriptor(key: "author", ascending: true)]

    ...

    return fetchedResultsController
}()

To update the table view when the order of the quotes changes, we need to revisit the controller(_:didChange:at:for:newIndexPath:) method one last time. When the change type is equal to move, we move the row of the affected managed object by removing a row at its old position and inserting a row at its new position.

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    switch (type) {
    case .insert:
        ...
    case .delete:
        ...
    case .update:
        ...
    case .move:
        if let indexPath = indexPath {
            tableView.deleteRows(at: [indexPath], with: .fade)
        }

        if let newIndexPath = newIndexPath {
            tableView.insertRows(at: [newIndexPath], with: .fade)
        }
        break;
    }
}

Run the application and add a few quotes. Modify the author of one of the quotes to see the result of this chnage. The table view is updated to reflect the new order of the quotes it displays.

Adding Sections

In the last tutorial of this series, we add sections to the table view. Remember that the NSFetchedResultsController class was designed with table views in mind. Managing the sections of a table view is no problem for the NSFetchedResultsController class.

Next Episode "Add Sections to Table View With NSFetchedResultsController and Swift"

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By