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.
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.
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.
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.
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.