Fetching Records With Core Data

Fetching Records With Core Data: Type Methods

Fetching Records With Core Data
1 Fetching Records With Core Data: Type Methods

Developers often complain that Core Data has an arcane syntax and a complicated API. "It's tedious to work with Core Data." seems to be the general consensus. It's true that Core Data used to be difficult to use and the framework's syntax wasn't as elegant as it could be. That's something of the past, though. The more Core Data matures, the more I enjoy and appreciate the framework.

First impressions are difficult to change and it's therefore unsurprising that developers often fall back on third party libraries. Using a third party library to interact with a first party framework isn't something I recommend.

Many of us find fetching records from a persistent store to be clunky and tedious. Is that true? In this series, I'd like to show you how easy and elegant fetching records from a persistent store can be.

We start with a simple example every developer familiar with Core Data understands. Along the way, we add more complexity by introducing flexibility and dynamism. We also leverage generics to make sure we don't unnecessarily repeat ourselves. Core Data and generics play very well together. Let's start with a simple data model.

Setting the Stage

Setting Up the Project

Launch Xcode and create a project based on the Single View App template.

Setting Up the Project

Let's assume we're creating an application that tracks the user's workouts. Name the project Workouts and, to save a bit of time, check the Use Core Data checkbox at the bottom.

Setting Up the Project

Populating the Data Model

The data model isn't too complicated. This is what it looks like. We define three entities, Workout, Exercise, and Session.

Populating the Data Model

As you can see, a workout has a name and it's linked to zero or more exercises. An exercise can belong to only one workout. Completed workouts are saved as sessions hence the Session entity. A session stores the duration of the workout and it also keeps a reference to the workout.

Every entity I create defines three default attributes:

  • uuid of type String
  • createdAt of type Date
  • updatedAt of type Date

If your application's deployment target is iOS 11 or higher, you can use the brand new UUID attribute type for the uuid attribute.

This is a summary of the entities of the data model.

Workout

Workout Entity

Attributes - name of type String - uuid of type String - createdAt of type Date - updatedAt of type Date

Relationships - sessions with Session as its destination - exercises with Exercise as its destination

Exercise

Exercise Entity

Attributes - name of type String - uuid of type String - createdAt of type Date - updatedAt of type Date - duration of type Double - order of type Integer 16

Relationships - workout with Workout as its destination

Session

Session Entity

Attributes - uuid of type String - createdAt of type Date - updatedAt of type Date

Relationships - workout with Workout as its destination

Xcode automatically creates a class definition for us. Why is that? Open the data model by selecting it in the Project Navigator. Select the Workout entity and open the Data Model Inspector on the right. Notice that Codegen in the Class section is set to Class Definition. This means that Xcode generates a class definition for us. It's the default setting in Xcode 9 at the time of writing.

Codegen in Data Model Inspector

First Things First

Fetching data from a persistent store isn't difficult. Let me show you what it looks like. Before we start, though, we need to inject the managed object context of the persistent container in the root view controller of the window. Euh ... what? Don't worry. It's easy. This is what that looks like.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

    // Load Storybaord
    let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)

    // Instantiate Initial View Controller
    guard let viewController = storyboard.instantiateInitialViewController() as? ViewController else {
        fatalError()
    }

    // Configure View Controller
    viewController.managedObjectContext = persistentContainer.viewContext

    // Configure Window
    window?.rootViewController = viewController

    return true
}

In the application(_:didFinishLaunchingWithOptions:) method of the AppDelegate class, we instantiate the initial view controller of the main storyboard, an instance of the ViewController class. We inject the managed object context of the persistent container by setting the managedObjectContext property of the view controller. This only works if we set the ViewController instance as the root view controller of the application's window.

Before we can build and run the application, we need to declare a variable property, managedObjectContext, of type NSManagedObjectContext! in the ViewController class. To make sure we didn't make any mistakes, add a print statement in the view controller's viewDidLoad() method and run the application.

import UIKit
import CoreData

class ViewController: UIViewController {

    // MARK: - Properties

    var managedObjectContext: NSManagedObjectContext!

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        print(managedObjectContext)
    }

}

This is a quick and dirty setup, but it's fine for what I'm about to show you. It's time to fetch records.

Fetching Records

The persistent store is empty at the moment, but that isn't a problem for this tutorial. The goal is to show you how to easily and elegantly fetch records from a persistent store. Let's start with a simple fetch request.

In the viewDidLoad() method of the ViewController class, we create a fetch request by invoking the fetchRequest() class method of the Workout class. Remember that Xcode has generated the Workout class for us, a NSManagedObject subclass. The fetchRequest() class method returns a NSFetchRequest<Workout> instance.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

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

    do {
        // Peform Fetch Request
        let workouts = try managedObjectContext.fetch(fetchRequest)

        print(workouts)
    } catch {
        print("Unable to Fetch Workouts, (\(error))")
    }
}

We execute the fetch request by passing it to the fetch(_:) method of the NSManagedObjectContext instance. The fetch(_:) method is throwing, which means we need to wrap it in a do-catch statement and attach the try keyword to the method invocation.

Because the persistent store is empty at the moment, the result printed to Xcode's console is an empty array.

We've successfully executed a fetch request. However, by taking this approach to interact with Core Data, the code you write is verbose and hard to test. The approach I recommend is simpler and easier to test.

Type Methods

We start by creating a Swift file, Workout.swift, and define an extension for the Workout class. Add an import statement for the Core Data framework at the top.

import CoreData

extension Workout {

}

I want to encapsulate fetching of Workout records in the Workout class. Let's start by defining a class method, findAll(in:) that accepts a managed object context as its only argument. The managed object context we pass to findAll(in:) is the one executing the fetch request.

class func findAll(in managedObjectContext: NSManagedObjectContext) -> [Workout] {

}

The implementation should look familiar. We create a fetch request and hand it to the managed object context we pass to findAll(in:).

class func findAll(in managedObjectContext: NSManagedObjectContext) -> [Workout] {
    // Helpers
    var workouts: [Workout] = []

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

    do {
        // Perform Fetch Request
        workouts = try managedObjectContext.fetch(fetchRequest)
    } catch {
        print("Unable to Fetch Workouts, (\(error))")
    }

    return workouts
}

That's a good start, but there's room for improvement. Instead of handling the error in the findAll(in:) method, it's more useful to propagate it to the method's call site. This is easy to do by turning findAll(in:) into a throwing class method and omitting the do-catch statement.

class func findAll(in managedObjectContext: NSManagedObjectContext) throws -> [Workout] {
    // Helpers
    var workouts: [Workout] = []

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

    // Perform Fetch Request
    workouts = try managedObjectContext.fetch(fetchRequest)

    return workouts
}

If this isn't what you want, then you can implement a variation that silences any errors that are thrown. It looks something like this. Take a moment to understand what's going on.

class func findAll(in managedObjectContext: NSManagedObjectContext) -> [Workout] {
    // Create Fetch Request
    let fetchRequest: NSFetchRequest<Workout> = Workout.fetchRequest()

    return (try? managedObjectContext.fetch(fetchRequest)) ?? []
}

If we use the try? keyword and an error is thrown, the error is handled by turning the result into an optional value. This means that there's no need to wrap the throwing method call in a do-catch statement. But notice that we don't return an optional value as the result. If executing the fetch request fails for some reason, we return an empty array.

I don't recommend ignoring errors and I therefore prefer the first implementation of the findAll(in:) class method.

We can now update the implementation of the viewDidLoad() method of the ViewController class. The result is cleaner and less verbose.

override func viewDidLoad() {
    super.viewDidLoad()

    do {
        // Fetch Workouts
        let workouts = try Workout.findAll(in: managedObjectContext)

        print(workouts)
    } catch {
        print("Unable to Fetch Workouts, (\(error))")
    }
}

If we aren't interested in any errors that are thrown, we can still fall back to the try? keyword. That's the benefit of choosing for a throwing method.

override func viewDidLoad() {
    super.viewDidLoad()

    // Fetch Workouts
    if let workouts = try? Workout.findAll(in: managedObjectContext) {
        print(workouts)
    } else {
        print("Unable to Fetch Workouts")
    }
}

By opting for the try? keyword, we don't ignore the error. It simply means that we're not interested in the reason of the failed fetch request.

I frequently see developers print or log errors. I assume it gives them the feeling that they're handling the error in some way. While this is helpful during development, when things hit the fan an error is only useful if you take some action in response to the error.

Adding Flexibility

I hope this tutorial has shown you that it's very easy to factor out a fetch request into a class method. This makes it easier to test your code and it cleans up the call site substantially.

In the next tutorial, we take it to the next level by adding more flexibility and dynamism. We often need the ability to find specific records and it's also essential to have the option to sort the results of the fetch request. This is something Core Data needs to handle for us.

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By