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.

Fetching Data from a 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.