Up until now, we've used NSManagedObject
instances to represent and interact with records stored in the persistent store. This works fine, but the syntax is verbose, we lose autocompletion, and type safety is also an issue.
Remember from earlier in this series that the values of a managed object are accessed by invoking value(forKey:)
and setValue(_:forKey:)
.
record.value(forKey: "firstName")
record.setValue("Bart", forKey: "firstName")
Because value(forKey:)
returns an object of type Any?
, we need to cast the result to the type we expect, using optional binding.
if let name = record.value(forKey: "name") as? String {
print(name)
}
While we could improve these examples by replacing string literals with constants, there is an even better approach, subclassing NSManagedObject
.
Generating Subclasses
Download or clone the project we created in the previous tutorial and open it in Xcode.
git clone https://github.com/bartjacobs/MigratingADataModelWithCoreData
Replace the implementation of application(_:didFinishLaunchingWithOptions:)
with the updated implementation below.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let managedObjectContext = coreDataManager.managedObjectContext
// Seed Persistent Store
seedPersistentStoreWithManagedObjectContext(managedObjectContext)
// Create Fetch Request
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "List")
// Add Sort Descriptor
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
let records = try managedObjectContext.fetch(fetchRequest)
for record in records {
print((record as AnyObject).value(forKey: "name") ?? "no name")
}
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.userInfo)")
}
return true
}
We seed the persistent store with dummy data, fetch every list records from the persistent store, and print the name of each list record to the console. In this tutorial, I'd like to show you how we can better interact with Core Data by subclassing NSManagedObject
.
Prior to Xcode 8, developers manually created a NSManagedObject
subclass for each entity. This is no longer necessary. As of Xcode 8, there is a much cleaner solution. Revisit the data model, Lists 2.xcdatamodel, and select the List entity. Open the Data Model Inspector on the right and take a close look at the Class section. The Codegen field is the setting we are interested in. As of Xcode 8.1, this setting is set by default to Class Definition.
The possible options are Manual/None, Class Definition, and Category/Extension. By setting it to Class Definition, a NSManagedObject
subclass is automatically generated for the entity and stored in the DerivedData folder, not in the project itself. And that is a good thing.
Because the default setting is Class Definition, we can already access a class named List
without making any changes. But Core Data adds even more magic. Let me show you what that magic looks like.
Autocompletion and Type Safety
Creating a NSManagedObject
subclass for each entity has a number of benefits, including autocompletion, an elegant syntax, and type safety. Revisit AppDelegate.swift and take a look at the updated implementation of the seedPersistentStoreWithManagedObjectContext(_:)
method.
private func seedPersistentStoreWithManagedObjectContext(_ managedObjectContext: NSManagedObjectContext) {
guard !UserDefaults.standard.bool(forKey: didSeedPersistentStore) else { return }
let listNames = ["Home", "Work", "Leisure"]
for listName in listNames {
// Create List
if let list = createRecordForEntity("List", inManagedObjectContext: managedObjectContext) as? List {
// Populate List
list.name = listName
list.createdAt = NSDate()
// Add Items
for i in 1...10 {
// Create Item
if let item = createRecordForEntity("Item", inManagedObjectContext: managedObjectContext) as? Item {
// Set Attributes
item.name = "Item \(i)"
item.createdAt = NSDate()
item.completed = (i % 3 == 0)
// Set List Relationship
item.list = list
}
}
}
}
do {
// Save Managed Object Context
try managedObjectContext.save()
} catch {
print("Unable to save managed object context.")
}
// Update User Defaults
UserDefaults.standard.set(true, forKey: didSeedPersistentStore)
}
If you take a close look at the method's implementation, you see that we cast the records to the NSManagedObject
subclasses Xcode has generated for us. This means that we no longer need to access properties using value(forKey:)
and setValue(_:forKey:)
. We can take advantage of autocompletion and we can also benefit from Swift's type safety.
if let list = createRecordForEntity("List", inManagedObjectContext: managedObjectContext) as? List {
// Populate List
list.name = listName
list.createdAt = NSDate()
...
}
Relationships also benefit from NSManagedObject
subclasses. Instead of using setValue(_:forKey:)
to set the list record of an item record, we set the list
property of the item record.
item.list = list
We need to make two changes in application(_:didFinishLaunchingWithOptions:)
. A NSManagedObject
subclass makes it very easy to fetch the records for a particular entity. Every NSManagedObject
subclass defines a fetchRequest()
method that returns an instance of the NSFetchRequest
class. Note that we specify the type of records the fetch request returns.
// Create Fetch Request
let fetchRequest: NSFetchRequest<List> = List.fetchRequest()
The result is that the fetch(_:)
method of the NSManagedObjectContext
class returns an array of List
records instead of generic NSManagedObject
instances. The benefit is that we can treat the record
objects in the do
clause of the do-catch
statement as List
instances.
do {
let records = try managedObjectContext.fetch(fetchRequest)
for record in records {
print(record.name ?? "no name")
}
} catch {
...
}
This is what the updated implementation of application(_:didFinishLaunchingWithOptions:)
looks like.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let managedObjectContext = coreDataManager.managedObjectContext
// Seed Persistent Store
seedPersistentStoreWithManagedObjectContext(managedObjectContext)
// Create Fetch Request
let fetchRequest: NSFetchRequest<List> = List.fetchRequest()
// Add Sort Descriptor
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
let records = try managedObjectContext.fetch(fetchRequest)
for record in records {
print(record.name ?? "no name")
}
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.userInfo)")
}
return true
}
Should You Use NSManagedObject Subclasses?
Nothing prevents you from working with NSManagedObject
instances. However, I hope this tutorial has convinced you of the benefits of NSManagedObject
subclasses. Since Xcode 8 adds the ability to automatically generate a NSManagedObject
subclass for each entity, there are very few arguments not to use them. In the rest of this series, we use NSManagedObject
subclasses to interact with Core Data model objects.
Questions? Leave them in the comments below or reach out to me on Twitter. You can download the source files of the tutorial from GitHub.
Now that you know what Core Data is and how the Core Data stack is set up, it's time to write some code. If you're serious about Core Data, check out Mastering Core Data With Swift. We build an application that is powered by Core Data and you learn everything you need to know to use Core Data in your own projects.