In the previous episode of Adding Core Data to an Existing Swift Project, we saved data to the persistent store, a SQLite database. In this episode, we fetch the books stored in the persistent store and update the label of the books view controller.
Performing a Fetch Request
Open BooksViewController.swift and navigate to the fetchBooks()
method. Performing a fetch request isn't difficult, but it requires a few steps. To fetch data from a persistent store, we need a fetch request, an instance of the NSFetchRequest
class. We ask the Book
class for an NSFetchRequest
instance by invoking its fetchRequest()
method. This method is automatically generated for us. We inform the compiler what type of records the fetch request returns.
// MARK: - Helper Methods
private func fetchBooks() {
// Create Fetch Request
let fetchRequest: NSFetchRequest<Book> = Book.fetchRequest()
}
We never directly access the persistent store. As I mentioned earlier in this series, the managed object context is the object we use to interact with Core Data. We wrap the code for executing the fetch request in a closure and pass the closure to the perform(_:)
method of the persistent container's managed object context. Let me explain what is happening.
// MARK: - Helper Methods
private func fetchBooks() {
// Create Fetch Request
let fetchRequest: NSFetchRequest<Book> = Book.fetchRequest()
// Perform Fetch Request
persistentContainer.viewContext.perform {
}
}
Threading is covered in detail in Core Data Fundamentals. What you need to remember for now is that by invoking the fetch request in the closure of the perform(_:)
method, we access the managed object context from the thread it is associated with. Don't worry if that doesn't make any sense as it is less important for this episode.
The closure that is passed to the perform(_:)
method is executed asynchronously. It doesn't block the thread the method is called from, the main thread in this example.
To execute the fetch request, we call execute()
on the NSFetchRequest
instance. Because execute()
is a throwing method, we prefix the invocation with the try
keyword and wrap it in a do-catch
statement. If the execute()
method is successful, it returns an array of Book
instances. We use the result of the fetch request to update the text
property of booksLabel
. We print any errors to the console.
// MARK: - Helper Methods
private func fetchBooks() {
// Create Fetch Request
let fetchRequest: NSFetchRequest<Book> = Book.fetchRequest()
// Perform Fetch Request
persistentContainer.viewContext.perform {
do {
// Execute Fetch Request
let result = try fetchRequest.execute()
// Update Books Label
self.booksLabel.text = "\(result.count) Books"
} catch {
print("Unable to Execute Fetch Request, \(error)")
}
}
}
Build and run the application to see the result. The books view controller displays the number of books that are stored in the persistent store.
Observing a Managed Object Context
There is a problem we need to address. When we add a book to the persistent store, the label of the books view controller isn't updated. The fetchBooks()
method is executed only once, in the view controller's viewDidLoad()
method.
We have a few options to address this issue. We could invoke the fetchBooks()
method in the view controller's viewWillAppear(_:)
method, but that isn't a solution I like. A better solution is observing the managed object context for changes. What does that mean?
Every time one of the managed objects of the managed object context changes, the managed object context posts a Notification.Name.NSManagedObjectContextDidChange
notification. The managed object context also posts a notification, Notification.Name.NSManagedObjectContextDidSave
, when it performed a save operation. That is the notification we are interested in.
We add the books view controller as an observer of the Notification.Name.NSManagedObjectContextDidSave
notifications by invoking the addObserver(forName:object:queue:using:)
method. Notice that the second argument of addObserver(forName:object:queue:using:)
is the managed object context the view controller is interested in. This is very important if you are working with multiple managed object contexts. The third argument of addObserver(forName:object:queue:using:)
is the operation queue that is used to execute the closure, the fourth argument. In the closure, the books view controller invokes the fetchBooks()
method.
// MARK: - View Life Cycle
override func viewDidLoad() {
...
// Observe Managed Object Context
NotificationCenter.default.addObserver(forName: .NSManagedObjectContextDidSave,
object: persistentContainer.viewContext,
queue: .main) { [weak self] _ in
self?.fetchBooks()
}
}
We remove the view controller as an observer in the deinitializer of the BooksViewController
class.
deinit {
// Remove Observer
NotificationCenter.default.removeObserver(self)
}
Build and run the application, add a book, and verify that the label of the books view controller reflects the number of books that are stored in the persistent store.
What's Next?
In this series, you learnt how to add Core Data to an existing Swift project. You added a data model to the project and defined an entity with attributes. You learnt how to add data to a persistent store and how to fetch data from a persistent store. If you want to learn more about Apple's popular persistence framework, then I encourage you to continue your journey with Core Data Fundamentals, an in-depth series on Core Data.