Core Data With NSFetchedResultsController and Swift

Populate a Table View With NSFetchedResultsController and Swift

Even though you don't need to use the NSFetchedResultsController class to populate a table view, I find myself using it quite a bit in Core Data applications. Swift has substantially improved support for Core Data and the NSFetchedResultsController class also benefits from these optimizations.

In this tutorial, we create an application that manages a list of quotes. We populate a table view with quotes using the NSFetchedResultsController class. The tools we use are Swift 4 and Xcode 9.

Setting Up the Project

Create a new project in Xcode and choose the Single View Application template. Name the project Quotes and make sure the Use Core Data checkbox is unchecked. We won't be using Xcode's Core Data template.

Setting Up the Project In Xcode

Setting Up the Project In Xcode

Adding the Data Model

Create a new file and choose the Data Model template in the iOS > Core Data section.

Creating the Data Model In Xcode

Name the data model Quotes and click Create.

Creating the Data Model In Xcode

Creating the Quote Entity

Open Quotes.xcdatamodeld and add an entity, Quote. Add three attributes:

  • author of type String
  • contents of type String
  • createdAt of type Double

Creating the Quote Entity

Creating the User Interface

As I mentioned in the introduction, we display the quotes in a table view. Open ViewController.swift and declare three outlets:

  • messageLabel of type UILabel!
  • tableView of type UITableView!
  • activityIndicatorView of type UIActivityIndicatorView!
import UIKit

class ViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var messageLabel: UILabel!
    @IBOutlet var tableView: UITableView!
    @IBOutlet var activityIndicatorView: UIActivityIndicatorView!

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

Open Main.storyboard and embed the view controller of the View Controller Scene in a navigation controller. Add a table view, a label, and an activity indicator view to the view of the view controller. Don't forget to connect the user interface elements with the outlets we declared a moment ago. And make the view controller the delegate and the data source of the table view by connecting the delegate and dataSource outlets of the table view.

Creating the User Interface

Create a new UITableViewCell subclass and name it QuoteTableViewCell.

Creating a UITableViewCell Subclass

In QuoteTableViewCell.swift, declare two outlets:

  • authorLabel of type UILabel!
  • contentsLabel of type UILabel!

The class also defines a static constant, reuseIdentifier, which we use as the reuse identifier of the table view cell.

import UIKit

class QuoteTableViewCell: UITableViewCell {

    // MARK: - Properties

    static let reuseIdentifier = "QuoteCell"

    // MARK: -

    @IBOutlet var authorLabel: UILabel!
    @IBOutlet var contentsLabel: UILabel!

    // MARK: - Initialization

    override func awakeFromNib() {
        super.awakeFromNib()
    }

}

Revisit Main.storyboard, select the table view, and set Prototype Cells to 1. Select the prototype cell, open the Identity Inspector on the right, and set Class to QuoteTableViewCell.

Creating a Prototype Cell

Add two labels to the content view of the table view cell, position them as shown below, add the necessary constraints, and don't forget to connect the labels to the outlets of the QuoteTableViewCell class.

Laying Out the Quote Table View Cell

Last but not least, select the prototype cell and, in the Attributes Inspector on the right, set Identifier to QuoteCell.

Populating the Table View

To populate the table view, we need to implement the UITableViewDataSource protocol. Open ViewController.swift and define a property to store the quotes. This property is only temporary. The fetched results controller is going to be in charge of managing the data for the table view.

var quotes = [Quote]()

Create an extension for the ViewController class and implement the UITableViewDataSource protocol as shown below.

extension ViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return quotes.count
    }

    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")
        }

        // Fetch Quote
        let quote = quotes[indexPath.row]

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

        return cell
    }

}

If you don't use a NSFetchedResultsController instance to manage the data of the table view, you need to make sure the table view is updated when the data changes. For example, we want to hide the table view if the application doesn't contain any quotes. Define a didSet property observer for the quotes property in which we invoke a helper method, updateView().

var quotes = [Quote]() {
    didSet {
        updateView()
    }
}

The implementation of updateView() is straightforward as you can see below.

private func updateView() {
    let hasQuotes = quotes.count > 0

    tableView.isHidden = !hasQuotes
    messageLabel.isHidden = hasQuotes
}

We need to add a few more details. In viewDidLoad(), we invoke another helper method, setupView().

override func viewDidLoad() {
    super.viewDidLoad()

    setupView()
}

In this helper method, we invoke setupMessageLabel() and updateView().

private func setupView() {
    setupMessageLabel()

    updateView()
}

// MARK: -

private func setupMessageLabel() {
    messageLabel.text = "You don't have any quotes yet."
}

Build and run the application to see what we have so far. You should see the message label being displayed. The activity indicator view is also visible. That is the next thing we fix.

Run Application

Setting Up the Core Data Stack

We are building a Core Data application and we don't have a Core Data stack yet. What is that about? Open ViewController.swift and declare a property of type NSPersistentContainer. We initialize the persistent container by passing in the name of the data model we created earlier. Don't forget to add an import statement for the Core Data framework at the top.

If the NSPersistentContainer container class is new to you, then you may want to read more about that first.

import UIKit
import CoreData

class ViewController: UIViewController {

    ...

    private let persistentContainer = NSPersistentContainer(name: "Quotes")

    ...

}

Even though we have created a NSPersistentContainer instance, the Core Data stack is not ready for us to use. We need to add the persistent store to the persistent store coordinator. We do this in the viewDidLoad() method as shown below.

override func viewDidLoad() {
    super.viewDidLoad()

    persistentContainer.loadPersistentStores { (persistentStoreDescription, error) in
        if let error = error {
            print("Unable to Load Persistent Store")
            print("\(error), \(error.localizedDescription)")

        } else {
            self.setupView()
        }
    }
}

The loadPersistentStores(completionHandler:) method asynchronously loads the persistent store(s) and adds it to the persistent store coordinator. When this operation finishes, the completion handler of the loadPersistentStores(completionHandler:) method is invoked. Even though the persistent store is added to the persistent store coordinator on a background queue, the completion handler is always invoked on the calling thread.

In the completion handler, we make sure no errors popped up. If everything went smoothly, we invoke setupView(). Before we can run the application again to see the result, we need to revisit the main storyboard.

Select the activity indicator view and, in the Attributes Inspector on the right, check the Animating and Hides When Stopped checkboxes.

Updating the User Interface Again

Select the table view and the message label and, in the Attributes Inspector on the right, check the Hidden checkbox.

Updating the User Interface Again

In the updateView() method of the ViewController class, we hide the activity indicator view by invoking stopAnimating().

private func updateView() {
    let hasQuotes = quotes.count > 0

    tableView.isHidden = !hasQuotes
    messageLabel.isHidden = hasQuotes

    activityIndicatorView.stopAnimating()
}

Creating the Fetched Results Controller

If we don't use a fetched results controller, we need to manually fetch and manage the quotes displayed in the table view. The NSFetchedResultsController class makes this much easier. We first need to create a lazy property for the fetched results controller.

Notice that we specify the type of objects the fetched results controller manages. Core Data and Swift work very well together. It makes working with NSManagedObject subclasses much easier.

fileprivate lazy var fetchedResultsController: NSFetchedResultsController<Quote> = {
    // Create Fetch Request
    let fetchRequest: NSFetchRequest<Quote> = Quote.fetchRequest()

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

    // Create Fetched Results Controller
    let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)

    // Configure Fetched Results Controller
    fetchedResultsController.delegate = self

    return fetchedResultsController
}()

To create an instance of the NSFetchedResultsController class, we need a fetch request. We ask the Quote class for a NSFetchRequest<Quote> object. We configure the fetch request by setting its sortDescriptors property. We sort the quotes based on the value of the createdAt property.

We then initialize the NSFetchedResultsController instance. The initializer defines four parameters:

  • a fetch request
  • a managed object context
  • a key path for creating sections
  • a cache name for optimizing performance

The managed object context we pass to the initializer is the managed object context that is used to perform the fetch request. The key path and cache name are not important for this discussion.

Before we return the fetched results controller from the closure, we set the delegate to self, the view controller.

Because the fetchedResultsController property is a lazy property, it isn't a problem that we access the viewContext property of the persistent container. But this means that we need to make sure we access the fetchedResultsController property after the Core Data stack is fully initialized.

Performing a Fetch

The fetched results controller doesn't perform a fetch if we don't tell it to. We should perform a fetch when the Core Data stack is ready to use. Performing a fetch is as simple as invoking performFetch() on the fetched results controller. We do this in the completion handler of the loadPersistentStores(completionHandler:) method.

override func viewDidLoad() {
    super.viewDidLoad()

    persistentContainer.loadPersistentStores { (persistentStoreDescription, error) in
        if let error = error {
            print("Unable to Load Persistent Store")
            print("\(error), \(error.localizedDescription)")

        } else {
            self.setupView()

            do {
                try self.fetchedResultsController.performFetch()
            } catch {
                let fetchError = error as NSError
                print("Unable to Perform Fetch Request")
                print("\(fetchError), \(fetchError.localizedDescription)")
            }

            self.updateView()
        }
    }
}

Notice that we also invoke updateView at the end of the completion handler to update the view.

We can now get rid of the quotes property and ask the fetched results controller for the data we need. Remove the quotes property and update the implementation of the UITableViewDataSource protocol as shown below.

extension ViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let quotes = fetchedResultsController.fetchedObjects else { return 0 }
        return quotes.count
    }

    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")
        }

        // Fetch Quote
        let quote = fetchedResultsController.object(at: indexPath)

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

        return cell
    }

}

To obtain the number of quotes the fetched results controller fetched from the persistent store, we ask it for the value of its fetchedObjects property. If it has any managed objects for us, we return the number.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    guard let quotes = fetchedResultsController.fetchedObjects else { return 0 }
    return quotes.count
}

A fetched results controller is perfectly suited for managing the contents of a table view. This means it has no problem working with sections and rows. If we ask it for the managed object at a particular index path, it knows exactly what we need. This means we only need to update one line in the tableView(_:cellForRowAt:) method.

// Fetch Quote
let quote = fetchedResultsController.object(at: indexPath)

Before we can build and run the application, we need to update the updateView() method.

private func updateView() {
    var hasQuotes = false

    if let quotes = fetchedResultsController.fetchedObjects {
        hasQuotes = quotes.count > 0
    }

    tableView.isHidden = !hasQuotes
    messageLabel.isHidden = hasQuotes

    activityIndicatorView.stopAnimating()
}

And we need to create an extension for the ViewController class to make it conform to the NSFetchedResultsControllerDelegate protocol.

extension ViewController: NSFetchedResultsControllerDelegate {}

Because every method of the NSFetchedResultsControllerDelegate protocol is optional, we can leave the extension empty for now. We implement the methods of the protocol later.

Build and Run

Build and run the application. The user interface hasn't changed much, but it is now powered by a fetched results controller.

Build and Run

In the next tutorial, we implement the methods of the NSFetchedResultsControllerDelegate protocol. This will enable us to update the table view if quotes are added, updated, or removed.

Next Episode "Implement the NSFetchedResultsControllerDelegate Protocol With Swift"

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By