Core Data Fundamentals

Fetch Those Notes

In this episode, we fetch the user's notes from the persistent store and display them in a table view. The notes view controller is in charge of these tasks.

Before We Start

I've already updated the storyboard with a basic user interface. Let me walk you through it. The view of the notes view controller contains a label, for displaying a message to the user, and a table view, for listing the user's notes. The label and the table view are embedded in another view, the notes view. The reason for this becomes clear later in this series. Don't worry about it for now.

Notes View Controller

The table view has one prototype cell of type NoteTableViewCell. The NoteTableViewCell class defines three outlets, a label for the title of the note, a label that displays the time and date when the note was last updated, and a label for displaying a preview of the note's contents.

NoteTableViewCell.swift

import UIKit

class NoteTableViewCell: UITableViewCell {

    // MARK: - Static Properties

    static let reuseIdentifier = "NoteTableViewCell"

    // MARK: - Properties

    @IBOutlet var titleLabel: UILabel!
    @IBOutlet var contentsLabel: UILabel!
    @IBOutlet var updatedAtLabel: UILabel!

    // MARK: - Initialization

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

}

As you may have guessed, the notes view controller is the delegate and data source of the table view.

If we run the application, we see a message that tells us we don't have any notes yet, even though we successfully created a note in the previous episode. Let's fix that.

You don't have any notes yet.

Fetching Notes

To display the user's notes, we first need to fetch them from the persistent store. Open NotesViewController.swift and add an import statement for the Core Data framework.

NotesViewController.swift

import UIKit
import CoreData

We declare a property for storing the notes we fetch from the persistent store. We name the property notes and it should be of type [Note]?. We also define a property observer because we want to update the user interface every time the value of the notes property changes. In the property observer, we invoke a helper method, updateView().

NotesViewController.swift

private var notes: [Note]? {
    didSet {
        updateView()
    }
}

In viewDidLoad(), we fetch the notes from the persistent store by invoking another helper method, fetchNotes().

NotesViewController.swift

override func viewDidLoad() {
    super.viewDidLoad()

    title = "Notes"

    setupView()

    fetchNotes()
}

In the fetchNotes() method, we fetch the user's notes from the persistent store.

NotesViewController.swift

private func fetchNotes() {

}

The first ingredient we need is a fetch request. Whenever you need information from the persistent store, you need a fetch request, an instance of the NSFetchRequest class.

NotesViewController.swift

// Create Fetch Request
let fetchRequest: NSFetchRequest<Note> = Note.fetchRequest()

Notice that we specify the type we expect from the fetch request. The compiler takes care of the nitty-gritty details for us.

We want to sort the notes based on the value of the updatedAt property. In other words, we want to show the most recently updated note at the top of the table view. For that to work, we need to tell the fetch request how it should sort the results it receives from the persistent store.

We create a sort descriptor, an instance of the NSSortDescriptor class, and set the sortDescriptors property of the fetch request. The sortDescriptors property is an array, which means we could specify multiple sort descriptors. The sort descriptors are evaluated based on the order in which they appear in the array.

NotesViewController.swift

// Configure Fetch Request
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Note.updatedAt), ascending: false)]

Remember that we never directly access the persistent store. We execute the fetch request using the managed object context of the Core Data manager. We wrap the code for executing the fetch request in a closure, which is the argument of the performAndWait(_:) method of the NSManagedObjectContext class. What's going on here?

NotesViewController.swift

// Perform Fetch Request
coreDataManager.managedObjectContext.performAndWait {

}

We take a closer look at the reasons for doing this later in this series. What you need to remember for now is that by invoking the fetch request in the closure of the performAndWait(_:) method, we access the managed object context on the thread it's associated with. Don't worry if that doesn't make any sense yet. It will click once we discuss threading much later in this series.

What you need to understand now is that the closure of the performAndWait(_:) method is executed synchronously hence the wait keyword in the method name. It blocks the thread the method is invoked from, the main thread in this example. That isn't an issue, though. Core Data is performant enough that we can trust that this isn't a problem for now. We can optimize this later should we run into performance issues.

Executing a fetch request is a throwing operation, which is why we wrap it in a do-catch statement. To execute the fetch request, we invoke execute() on the fetch request, a throwing method. We update the notes property with the results of the fetch request and reload the table view. If any errors pop up, we print them to the console.

NotesViewController.swift

// Perform Fetch Request
coreDataManager.managedObjectContext.performAndWait {
    do {
        // Execute Fetch Request
        let notes = try fetchRequest.execute()

        // Update Notes
        self.notes = notes

        // Reload Table View
        self.tableView.reloadData()

    } catch {
        let fetchError = error as NSError
        print("Unable to Execute Fetch Request")
        print("\(fetchError), \(fetchError.localizedDescription)")
    }
}

This is what the fetchNotes() method looks like.

NotesViewController.swift

private func fetchNotes() {
    // Create Fetch Request
    let fetchRequest: NSFetchRequest<Note> = Note.fetchRequest()

    // Configure Fetch Request
    fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Note.updatedAt), ascending: false)]

    // Perform Fetch Request
    coreDataManager.managedObjectContext.performAndWait {
        do {
            // Execute Fetch Request
            let notes = try fetchRequest.execute()

            // Update Notes
            self.notes = notes

            // Reload Table View
            self.tableView.reloadData()

        } catch {
            let fetchError = error as NSError
            print("Unable to Execute Fetch Request")
            print("\(fetchError), \(fetchError.localizedDescription)")
        }
    }
}

That was probably one of the most complicated sections of this series. Make sure you understand the what and why of the fetch request. Watch this episode again if necessary because it's important that you understand what's going on. Remember that you can ignore the performAndWait(_:) method for now, but make sure you understand how to create and execute a fetch request.

Displaying Notes

Before we move on, I want to implement a computed property that tells us if we have notes to display. The implementation of the hasNotes property is pretty straightforward.

NotesViewController.swift

private var hasNotes: Bool {
    guard let notes = notes else { return false }
    return notes.count > 0
}

In the updateView() method, we use the value of the hasNotes property to update the user interface.

NotesViewController.swift

private func updateView() {
    tableView.isHidden = !hasNotes
    messageLabel.isHidden = hasNotes
}

Last but not least, we need to update the implementation of the UITableViewDataSource protocol. The implementation of numberOfSections(in:) is easy. The application returns 1 if it has notes, otherwise it returns 0.

NotesViewController.swift

func numberOfSections(in tableView: UITableView) -> Int {
    return hasNotes ? 1 : 0
}

The same is true for the implementation of tableView(_:numberOfRowsInSection:). The application returns the number of notes if it has notes to display, otherwise it returns 0.

NotesViewController.swift

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

The implementation of tableView(_:cellForRowAt:) is more interesting. We first fetch the note that corresponds with the value of the indexPath parameter. We then dequeue a note table view cell and we populate the table view cell with the data of the note.

NotesViewController.swift

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // Fetch Note
    guard let note = notes?[indexPath.row] else {
        fatalError("Unexpected Index Path")
    }

    // Dequeue Reusable Cell
    guard let cell = tableView.dequeueReusableCell(withIdentifier: NoteTableViewCell.reuseIdentifier, for: indexPath) as? NoteTableViewCell else {
        fatalError("Unexpected Index Path")
    }

    // Configure Cell
    cell.titleLabel.text = note.title
    cell.contentsLabel.text = note.contents
    cell.updatedAtLabel.text = updatedAtDateFormatter.string(from: note.updatedAt)

    return cell
}

But it seems that we have a problem. Because we're dealing with Core Data, the type of the updatedAt property is Date?. The date formatter we use to convert the date to a string doesn't like that.

Running Into Type Issues

The solution is simple. We create an extension for the Note class and define computed properties for the updatedAt and createdAt properties, which return a Date instance. Create a new group, Extensions, in the Core Data group and add a file named Note.swift. We import Foundation and create an extension for the Note class. The implementation of the computed properties is straightforward. If updatedAt or createdAt is equal to nil, we return the current date and time.

Note.swift

import Foundation

extension Note {

    var updatedAtAsDate: Date {
        return updatedAt ?? Date()
    }

    var createdAtAsDate: Date {
        return createdAt ?? Date()
    }

}

Do we need an extension for this? Can't we use the nil-coalescing operator in the tableView(_:cellForRowAt:) method? We could, but I don't want the view controller to know about this implementation detail.

We can now update the implementation of tableView(_:cellForRowAt:).

NotesViewController.swift

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // Fetch Note
    guard let note = notes?[indexPath.row] else {
        fatalError("Unexpected Index Path")
    }

    // Dequeue Reusable Cell
    guard let cell = tableView.dequeueReusableCell(withIdentifier: NoteTableViewCell.reuseIdentifier, for: indexPath) as? NoteTableViewCell else {
        fatalError("Unexpected Index Path")
    }

    // Configure Cell
    cell.titleLabel.text = note.title
    cell.contentsLabel.text = note.contents
    cell.updatedAtLabel.text = updatedAtDateFormatter.string(from: note.updatedAtAsDate)

    return cell
}

And with that change, we're ready to take the application for a spin. You should now see the notes of the user displayed in the table view.

Displaying the User's Notes

You may notice that the current implementation has a significant flaw. The application fetches the user's notes once. And that's it. The table view isn't updated if you add a new note. We fix this problem in the next episode.

In this episode, you learned how to fetch data from the persistent store. In the next episode, you learn how to update notes and automatically update the table view.

Next Episode "Fix That Mistake"

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By