In the previous tutorial, we created a list record and pushed it to the persistent store by saving its managed object context. The persistent store coordinator handled the nitty-gritty details of inserting the list record into the persistent store. This tutorial focuses on reading and updating records.
Before We Start
If you want to follow along, you can download the source files at the bottom of this tutorial.
Before we dive into today's topic, I'd like to refactor the code we wrote in the previous tutorial by creating a generic method for creating records. Open AppDelegate.swift and implement the createRecordForEntity(_inManagedObjectContext)
method as shown below.
private func createRecordForEntity(_ entity: String, inManagedObjectContext managedObjectContext: NSManagedObjectContext) -> NSManagedObject? {
// Helpers
var result: NSManagedObject?
// Create Entity Description
let entityDescription = NSEntityDescription.entity(forEntityName: entity, in: managedObjectContext)
if let entityDescription = entityDescription {
// Create Managed Object
result = NSManagedObject(entity: entityDescription, insertInto: managedObjectContext)
}
return result
}
The implementation should look familiar if you read the previous tutorial. With createRecordForEntity(_inManagedObjectContext)
implemented, update application(_:didFinishLaunchingWithOptions:)
as shown below. We create a list record using the new helper method.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let managedObjectContext = coreDataManager.managedObjectContext
let _ = createRecordForEntity("List", inManagedObjectContext: managedObjectContext)
do {
// Save Managed Object Context
try managedObjectContext.save()
} catch {
print("Unable to save managed object context.")
}
return true
}
This looks much better. However, we don't want to create a list record every time we run the application. To avoid this scenario, we need to fetch every list record from the persistent store and only create a list record if the persistent store doesn't contain any list records yet. To implement this solution, we first need to learn how to fetch records from a persistent store.
How to Fetch a Record From a Persistent Store
To read or fetch records from a persistent store, we make use of the NSFetchRequest
class. To avoid cluttering application(_:didFinishLaunchingWithOptions:)
, we create another helper method, fetchRecordsForEntity(_inManagedObjectContext)
. This is what the implementation of fetchRecordsForEntity(_inManagedObjectContext)
looks like.
private func fetchRecordsForEntity(_ entity: String, inManagedObjectContext managedObjectContext: NSManagedObjectContext) -> [NSManagedObject] {
// Create Fetch Request
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entity)
// Helpers
var result = [NSManagedObject]()
do {
// Execute Fetch Request
let records = try managedObjectContext.fetch(fetchRequest)
if let records = records as? [NSManagedObject] {
result = records
}
} catch {
print("Unable to fetch managed objects for entity \(entity).")
}
return result
}
To fetch records from the persistent store, we use the NSFetchRequest
class. We create an instance of the class by invoking init(entityName:)
, passing in the name of the entity we are interested in. In fetchRecordsForEntity(_inManagedObjectContext)
, we fetch every record for the entity we pass in as an argument and create an empty array of type [NSManagedObject]
to store the result of the fetch request.
We execute the fetch request by passing it as an argument of the fetch(_:)
method of the NSManagedObjectContext
class. Because the fetch(_:)
method is a throwing method, we wrap it in a do-catch
statement. The return value of the fetch(_:)
method is assigned to records
.
In application(_:didFinishLaunchingWithOptions:)
, we fetch every list record and only create a list record if none could be found in the persistent store. This is a pattern you will use frequently in application development. For example, you only want to create a user record if the user could not be found in the persistent store.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let managedObjectContext = coreDataManager.managedObjectContext
// Helpers
var list: NSManagedObject? = nil
// Fetch List Records
let lists = fetchRecordsForEntity("List", inManagedObjectContext: managedObjectContext)
if let listRecord = lists.first {
list = listRecord
} else if let listRecord = createRecordForEntity("List", inManagedObjectContext: managedObjectContext) {
list = listRecord
}
print("number of lists: \(lists.count)")
print("--")
if let list = list {
print(list)
} else {
print("unable to fetch or create list")
}
do {
// Save Managed Object Context
try managedObjectContext.save()
} catch {
print("Unable to save managed object context.")
}
return true
}
Take a moment to let the implementation of application(_:didFinishLaunchingWithOptions:)
sink in. Run the application several times in the simulator and inspect the output in the console. With the exception of the first time you run the application, this is what you should see in Xcode's console every time the application is run:
number of lists: 1
--
<List: 0x6000002856e0> (entity: List; id: 0xd000000000040000 <x-coredata://4E793E70-C09C-4E94-B31D-EFE3F6604E1D/List/p1> ; data: <fault>)
The output displays the number of list records in the persistent store and it also shows the list record we either fetched or created. The output also shows that the record's entity is the List entity.
Why is data set to <fault>? When data is fetched from a persistent store, Core Data tries to be as efficient as possible in terms of memory usage. It only fetches the data the application asked for. Data that is not required is turned into a fault. The data is available, but Core Data hasn't fetched it from the persistent store to save memory and improve performance. You don't believe me? Remove the print statement of the if
clause and replace it with the following to better understand how faulting works.
if let list = list {
print(list)
print(list.value(forKey: "name") ?? "no name")
print(list)
} else {
print("unable to fetch or create list")
}
The first print statement of the if
clause shows that the list record has a fault.
<List: 0x608000282fd0> (entity: List; id: 0xd000000000040000 <x-coredata://4E793E70-C09C-4E94-B31D-EFE3F6604E1D/List/p1> ; data: <fault>)
The second print statement of the if
clause prints the value of the record's name
attribute. This means that Core Data needs to fire or resolve the fault.
no name
The third print statement of the if
clause shows us that data is no longer a fault.
<List: 0x608000282fd0> (entity: List; id: 0xd000000000040000 <x-coredata://4E793E70-C09C-4E94-B31D-EFE3F6604E1D/List/p1> ; data: {
createdAt = nil;
items = "<relationship fault: 0x618000031b80 'items'>";
name = nil;
})
This is known as firing or resolving a fault. Core Data fetches the missing data from the persistent store to fill in the gaps. Note that we only see a fault if the list record is fetched from the persistent store. A newly created managed object doesn't have any faults because it hasn't been fetched from a persistent store.
Take a closer look at the last print statement. Have you noticed that the items
relationship is also a fault. The values of the name
and createdAt
attributes are resolved, but the items
relationship isn't. This is another Core Data optimization. Faulting is something you need to become familiar with if you plan to use Core Data. Most of the time, it's something you don't need to worry about. Because faulting is such an important feature of Core Data, we discuss it in detail in Mastering Core Data With Swift 3.
Updating Records
How to Update an Attribute of a Managed Object
The above output shows us that the list record we fetched from the persistent store doesn't have a name or creation date. We can fix that by updating the list record and saving the changes. To set the value of an attribute, we invoke setValue(_:forKey:)
on the managed object.
if let list = list {
print(list.value(forKey: "name") ?? "no name")
print(list.value(forKey: "createdAt") ?? "no creation date")
if list.value(forKey: "name") == nil {
list.setValue("Shopping List", forKey: "name")
}
if list.value(forKey: "createdAt") == nil {
list.setValue(Date(), forKey: "createdAt")
}
} else {
print("unable to fetch or create list")
}
Note that we only set the name
and createdAt
attributes if they don't have a value. Run the application to update the list record. This is what you see in the console if the record doesn't have a name or creation date.
number of lists: 1
--
no name
no creation date
To make sure the update was successfully pushed to the persistent store, we run the application again and inspect the output in the console. It should look something like this:
number of lists: 1
--
Shopping List
2017-03-07 14:01:28 +0000
Note that the values returned from value(forKey:)
are optionals. The documentation tells us that the return type of value(forKey:)
is Any?
. This isn't ideal and it is something we fix later in this series. What's important is that we have updated the list record and pushed the updates to the persistent store.
How to Update a Relationship of a Managed Object
Working with relationships is similar to working with attributes. There are a few differences you need to be aware of. Setting or updating a to-one relationship is identical to setting or updating an attribute. The only difference is that the value you pass to setValue(_:forKey:)
is another managed object.
Working with to-many and many-to-many relationships is different. The items
relationship of the list record, for example, is a set (NSSet
) of item records. Most developers new to Core Data expect an array or an ordered set.
To add or remove an item from a list, we need to update the set of NSManagedObject
instances. Let me show you how that works. In the example, we create an item record, set its attributes, set its list
relationship, and add it to the items
relationship of the list record.
if let list = list {
if list.value(forKey: "name") == nil {
list.setValue("Shopping List", forKey: "name")
}
if list.value(forKey: "createdAt") == nil {
list.setValue(Date(), forKey: "createdAt")
}
let items = list.mutableSetValue(forKey: "items")
// Create Item Record
if let item = createRecordForEntity("Item", inManagedObjectContext: managedObjectContext) {
// Set Attributes
item.setValue("Item \(items.count + 1)", forKey: "name")
item.setValue(Date(), forKey: "createdAt")
// Set Relationship
item.setValue(list, forKey: "list")
// Add Item to Items
items.add(item)
}
print("number of items: \(items.count)")
print("---")
for itemRecord in items {
print((itemRecord as AnyObject).value(forKey: "name") ?? "no name")
}
} else {
print("unable to fetch or create list")
}
Every time you run the application, a new item record is added to the list record. If you inspect the output in the console, you can see that the set of item records is unordered.
number of lists: 1
--
number of items: 6
---
Item 4
Item 6
Item 5
Item 2
Item 3
Item 1
To get the set of item records, we used a convenience method of the NSManagedObject
class, mutableSetValue(forKey:)
. This makes adding and removing records from a to-many or many-to-many relationship a bit easier.
In the example, we updated the list
relationship of the item record and we also updated the items
relationship of the list record. Remember from the data model, that these relationships are each other's inverse. Core Data is clever enough, though. We only need to set one of these relationships. If we set the list
relationship of the item record, Core Data automatically updates the items
relationship of the list record, and vice versa.
Improvements
The code we've written to interact with managed objects doesn't look pretty. This will change when we start working with NSManagedObject
subclasses later in this series. While you can work with NSManagedObject
instances, like we did in this tutorial, you will almost always work with NSManagedObject
subclasses. They add type safety and make working with Core Data records much more elegant.
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 Core Data Fundamentals. In this series, 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.