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