Mastering Core Data Swift
Mastering Core Data Swift
Bart Jacobs
* * * * *
This is a Leanpub book. Leanpub empowers authors and publishers with the
Lean Publishing process. Lean Publishing is the act of publishing an in-
progress ebook using lightweight tools and many iterations to get reader
feedback, pivot until you have the right book and build traction once you do.
* * * * *
In this book, we build Notes, an iOS application that manages a list of notes.
Notes is a simple iOS application, yet it contains all the ingredients we need to
learn about the Core Data framework, from creating and deleting records to
managing many-to-many relationships.
We also take a close look at the brains of a Core Data application, the data
model. We discuss data model versioning and migrations. These concepts
are essential for every Core Data application.
Core Data records are represented by managed objects. You learn how to
create them, fetch them from a persistent store, and delete them if they’re no
longer needed.
Mastering Core Data With Swift also covers a few more advanced topics.
Even though these topics are more advanced, they’re essential if you work
with Core Data. We talk in detail about the NSFetchedResultsController class
and, at the end of this book, I introduce you to the brand new
NSPersistentContainer class, a recent addition to the framework.
Last but not least, we take a deep dive into Core Data and concurrency, an
often overlooked topic. This is another essential topic for anyone working with
Core Data. Don’t skip this.
That’s a lot to cover, but I’m here to guide you along the way. If you have any
feedback or questions, reach out to me via email ([email protected]) or
Twitter (@_bartjacobs). I’m here to help.
If you’re new to Core Data, then I recommend reading every chapter of the
book. Over the years, I have taught thousands of developers about the Core
Data framework. From that experience, I developed a roadmap for teaching
Core Data. This book is the result of that roadmap.
Not everyone likes books. If you prefer video, then you may be interested in a
video course in which I teach the Core Data framework. The content is virtually
identical. The only difference is that you can see how I build Notes using the
Core Data framework. You can find the video course on the Cocoacasts
website.
1 What Is Core Data
Developers new to Core Data often don’t take the time to learn about the
framework. Not knowing what Core Data is, makes it hard and frustrating to
wrap your head around the ins and outs of the framework. I’d like to start by
spending a few minutes exploring the nature of Core Data and, more
importantly, explain to you what Core Data is and isn’t.
Core Data is the M in MVC, the model layer of your application. Even though
Core Data can persist data to disk, data persistence is actually an optional
feature of the framework. Core Data is first and foremost a framework for
managing an object graph.
You’ve probably heard and read about Core Data before taking this course.
That means that you may already know that Core Data is not a database and
that it manages your application’s object graph. Both statements are true. But
what do they really mean?
An object graph is nothing more than a collection of objects that are connected
with one another. The Core Data framework excels at managing complex
object graphs.
What Is an Object Graph
The Core Data framework takes care of managing the life cycle of the objects
in the object graph. It can optionally persist the object graph to disk and it also
offers a powerful interface for searching the object graph it manages.
But Core Data is much more than that. The framework adds a number of other
compelling features, such as input validation, data model versioning, and
change tracking.
Even though Core Data is a perfect fit for a wide range of applications, not
every application should use Core Data.
And if you’re looking for a SQLite wrapper, then Core Data is also not what you
need. For a lightweight, performant SQLite wrapper, I highly recommend Gus
Mueller’s FMDB. This robust, mature library provides an object-oriented
interface for interacting with SQLite.
If you wonder whether you need Core Data or SQLite, you’re asking the wrong
question. Remember that Core Data is not a database.
Core Data is responsible for the integrity of the object graph. It ensures the
object graph is kept up to date.
Drawbacks
Even though Core Data is a fantastic framework, there are several drawbacks.
These drawbacks are directly related to the nature of Core Data and how it
works.
Performance
Core Data can only do its magic because it keeps the object graph it manages
in memory. This means that it can only operate on records once they are in
memory. This is very different from performing a SQL query on a database. If
you want to delete thousands of records, Core Data first needs to load each
record into memory. It goes without saying that this results in memory and
performance issues if done incorrectly.
Multithreading
Another important limitation is the threading model of Core Data. The
framework expects to be run on a single thread. Fortunately, Core Data has
evolved dramatically over the years and the framework has put various
solutions in place to make working with Core Data in a multithreaded
environment much safer and much easier.
For applications that need to manage a complex object graph, Core Data is a
great fit. If you only need to store a handful of unrelated objects, then you may
be better off with a lightweight solution or the user defaults system.
2 Building Notes
Notes is a simple application for iOS that manages a list of notes. You can add
notes, update notes, and delete notes.
Building Notes
Users can also take advantage of categories to organize their notes. A user
can add, update, and delete categories. Each category has a color to make it
easier to see what category a note belongs to. A note can belong to one
category and a category can have multiple notes.
A note has zero or more tags. The tags of a note are listed below the title of
the note. Adding, updating, and removing tags is pretty straightforward.
The user’s notes are sorted by last modified date. The most recently modified
note appears at the top of the table view.
Even though Notes is a simple application, it’s ideal for learning the ropes of
the Core Data framework. The data model contains the ingredients of a typical
Core Data application with one-to-many and many-to-many relationships.
In this book, we primarily focus on the aspects that relate to Core Data. We
won’t focus on building the user interface unless it’s necessary to explain a
concept of the Core Data framework. That is Notes in a nutshell.
In the next chapter, we start our journey by exploring the Core Data stack, the
heart of every Core Data application.
3 Exploring the Core Data Stack
Earlier in this book, we learned what Core Data is and isn’t. In this chapter, we
zoom in on the building blocks of the Core Data framework.
As I mentioned earlier, it’s key that you understand how the various classes
that make Core Data tick play together. The star players of the Core Data
framework are:
This diagram shows how these classes relate to one another. We’ll use this
diagram as a guideline in this chapter.
This diagram shows that the managed object model is connected to the data
model. The data model is represented by a file in the application bundle that
contains the data schema of the application. This is something we revisit later
in this book when we start working with Core Data.
Managed Object Model
The data model is represented by a file in the application bundle that contains
the data schema of the application. The data schema is nothing more than a
collection of entities. An entity can have attributes and relationships, which
make up the data model of the application.
We explore the data model in more detail later. For now, remember that the
managed object model is an instance of the NSManagedObjectModel class and
represents the data model of the Core Data application.
The managed object context receives the model objects through a persistent
store coordinator as you can see in this diagram. A managed object context
keeps a reference to the persistent store coordinator of the application.
The managed object context is the object you interact with most. It creates,
reads, updates, and deletes model objects. From a developer’s perspective,
the NSManagedObjectContext class is the workhorse of the Core Data
framework.
But wait … what’s a parent managed object context? Later in this book, we
take a closer look at parent and child managed object contexts. Don’t worry
about this for now.
The above diagram also tells us that the persistent store coordinator is
connected to one or more persistent stores. What’s a persistent store?
Remember that Core Data manages an object graph. The framework is only
useful if the persistent store coordinator is connected to one or more persistent
stores.
Out of the box, Core Data supports three persistent store types:
- a SQLite database
- a binary store
- an in-memory store
Each persistent store type has its pros and cons. Most applications use a
SQLite database as their persistent store. As we saw in the previous chapter,
SQLite is lightweight and very fast. It’s great for mobile and desktop
applications.
Now that we know what the Core Data stack consists of, it’s time to explore
how it operates in an application.
After setting up the persistent store coordinator and the managed object
model, the workhorse of the Core Data stack is initialized, the managed
object context. Remember that a managed object context keeps a reference
to the persistent store coordinator.
The managed object context is the workhorse of the Core
Data stack.
With the Core Data stack set up, the application is ready to use Core Data to
interact with the application’s persistent store. In most cases, your application
interacts with the persistent store coordinator through the managed object
context.
You will rarely, if ever, directly interact with the persistent store coordinator or
the managed object model. As I mentioned earlier, the NSManagedObjectContext
class is the class you interact with most frequently.
The managed object context is used to create, read, update, and delete
records. When the changes made in the managed object context are saved,
the managed object context pushes them to the persistent store coordinator,
which sends the changes to the corresponding persistent store.
The managed object context pushes changes to the
persistent store coordinator, which sends them to the
persistent store.
Now that you know what Core Data is and how the Core Data stack is set up,
it’s time to write some code. In the next chapters, we create a Core Data stack
and explore the classes we discussed in this chapter.
4 Creating the Project
Before we set up the Core Data stack, we need to create the project for Notes.
Open Xcode and create a new project based on the Single View Application
template.
Name the project Notes, set Language to Swift, and, if you’re using Xcode 8,
set Devices to iPhone. Make sure Use Core Data is unchecked. We’re going
to start from scratch.
Choose where you want to store the project and click Create.
Creating the Project
- Application Delegate
- View Controllers
- Root View Controller
- Storyboards
- Resources
- Supporting Files
This is what the result looks like in the Project Navigator. That looks a lot
better. Doesn’t it?
For this project, I’ve set the Deployment Target of the project to 10.0. In the
next chapter, we set up the Core Data stack of the project.
5 Setting Up the Core Data Stack
It’s time to write some code. Had we checked the Use Core Data checkbox
during the setup of the project, Xcode would have put the code for the Core
Data stack in the application delegate. This is something I don’t like and we
won’t be cluttering the application delegate with the setup of the Core Data
stack.
Instead, we’re going to create a separate class responsible for setting up and
managing the Core Data stack. Create a new group and name it Managers.
Create a new Swift file in the Managers group and name the file
CoreDataManager.swift. The CoreDataManager class is in charge of the Core
Data stack of the application.
Choosing the Swift File Template
Creating CoreDataManager.swift
Creating CoreDataManager.swift
Replace the import statement for the Foundation framework with an import
statement for the Core Data framework.
1 import CoreData
Next, we define the class itself. Note that we mark the CoreDataManager class
as final. It’s not intended to be subclassed.
1 import CoreData
2
3 final class CoreDataManager {
4
5 }
1 import CoreData
2
3 final class CoreDataManager {
4
5 // MARK: - Properties
6
7 private let modelName: String
8
9 }
The designated initializer of the class accepts the name of the data model as
an argument.
1 import CoreData
2
3 final class CoreDataManager {
4
5 // MARK: - Properties
6
7 private let modelName: String
8
9 // MARK: - Initialization
10
11 init(modelName: String) {
12 self.modelName = modelName
13 }
14
15 }
Remember that we need to instantiate three objects to set up the Core Data
stack:
Remember that every parent managed object context keeps a reference to the
persistent store coordinator of the Core Data stack. This means we need to set
the persistentStoreCoordinator property of the managed object context.
1 // Configure Managed Object Context
2 managedObjectContext.persistentStoreCoordinator = self.persistentSto\
3 reCoordinator
We throw a fatal error if the application is unable to find the data model in the
application bundle or if we’re unable to instantiate the managed object model.
Why is that? Because this should never happen in production. If the data
model isn’t present in the application bundle or the application is unable to load
the data model from the application bundle, we have bigger problems to worry
about.
Notice that we ask the application bundle for the URL of a resource with an
momd extension. This is the compiled version of the data model. We discuss
the data model in more detail later in this book.
The Core Data stack is only functional once the persistent store is added to the
persistent store coordinator. We start by creating the URL for the persistent
store. There are several locations for storing the persistent store. In this
example, we store the persistent store in the Documents directory of the
application’s sandbox. But you could also store it in the Library directory.
We append sqlite to the name of the data model because we’re going to use a
SQLite database as the persistent store. Remember that Core Data supports
SQLite databases out of the box.
1 // Helpers
2 let fileManager = FileManager.default
3 let storeName = "\(self.modelName).sqlite"
4
5 // URL Documents Directory
6 let documentsDirectoryURL = fileManager.urls(for: .documentDirectory\
7 , in: .userDomainMask)[0]
8
9 // URL Persistent Store
10 let persistentStoreURL = documentsDirectoryURL.appendingPathComponen\
11 t(storeName)
The second parameter, the configuration, isn’t important for this discussion.
The fourth argument, the options dictionary, is something we discuss later in
this book.
If the persistent store coordinator cannot find a persistent store at the location
we specified, it creates one for us. If a persistent store already exists at the
specified location, it’s added to the persistent store coordinator. This means
that the persistent store is automatically created the first time a user launches
your application. The second time, Core Data looks for the persistent store,
finds it at the specified location, and adds it to the persistent store coordinator.
The framework handles this for you.
In the catch clause, we print the error to the console if the operation failed. We
return the persistent store coordinator from the closure.
1 private lazy var persistentStoreCoordinator: NSPersistentStoreCoordi\
2 nator = {
3 // Initialize Persistent Store Coordinator
4 let persistentStoreCoordinator = NSPersistentStoreCoordinator(ma\
5 nagedObjectModel: self.managedObjectModel)
6
7 // Helpers
8 let fileManager = FileManager.default
9 let storeName = "\(self.modelName).sqlite"
10
11 // URL Documents Directory
12 let documentsDirectoryURL = fileManager.urls(for: .documentDirec\
13 tory, in: .userDomainMask)[0]
14
15 // URL Persistent Store
16 let persistentStoreURL = documentsDirectoryURL.appendingPathComp\
17 onent(storeName)
18
19 do {
20 // Add Persistent Store
21 try persistentStoreCoordinator.addPersistentStore(ofType: NS\
22 SQLiteStoreType, configurationName: nil, at: persistentStoreURL, opt\
23 ions: nil)
24
25 } catch {
26 fatalError("Unable to Add Persistent Store")
27 }
28
29 return persistentStoreCoordinator
30 }()
We now have a working Core Data stack, but we’re currently assuming that
everything is working fine all the time. Later in this book, we make the Core
Data manager more robust. Right now we just want to set up a Core Data
stack to make sure we have something to work with.
Adding a Data Model
Before we can take the Core Data manager for a spin, we need to add a data
model to the project. Create a new group for the data model and name it Core
Data.
Create a new file and choose the Data Model template from the iOS > Core
Data section.
Notice that the extension of the data model is xcdatamodeld. This is different
from the extension we used earlier in the managedObjectModel property. The
xcdatamodeld file isn’t included in the compiled application. The
xcdatamodeld file is compiled into an momd file and it’s the latter that’s
included in the compiled application. Only what is absolutely essential is
included in the momd file.
Build and run the application and inspect the output in the console. The output
should looks something like this.
1 <NSManagedObjectContext: 0x6180001cdd40>
Great. That seems to work. In the next chapter, we use dependency injection
to pass the Core Data manager from the application delegate to the root view
controller.
6 Core Data and Dependency Injection
I’m not going to lie. I don’t like singletons. Singletons are fine if they’re used
correctly, but I don’t like singletons for convenience. They almost always lead
to problems down the line.
This means that the Core Data manager isn’t going to be a singleton. We’re
going to create an instance in the application delegate and inject it into the root
view controller. Dependency injection is surprisingly easy if you break it down
to its bare essentials.
We first open ViewController.swift and create a property for the Core Data
manager. The property is an optional because we set it after the view
controller is initialized. That’s a drawback I’m happy to accept.
ViewController.swift
1 import UIKit
2
3 class ViewController: UIViewController {
4
5 // MARK: - Properties
6
7 var coreDataManager: CoreDataManager?
8
9 ...
10
11 }
AppDelegate.swift
1 import UIKit
2
3 @UIApplicationMain
4 class AppDelegate: UIResponder, UIApplicationDelegate {
5
6 // MARK: - Properties
7
8 var window: UIWindow?
9
10 // MARK: -
11
12 private let coreDataManager = CoreDataManager(modelName: "Notes")
13
14 ...
15
16 }
AppDelegate.swift
1 // Load Storyboard
2 let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
3
4 // Instantiate Initial View Controller
5 guard let initialViewController = storyboard.instantiateInitialViewC\
6 ontroller() as? ViewController else {
7 fatalError("Unable to Configure Initial View Controller")
8 }
AppDelegate.swift
1 // Configure Initial View Controller
2 initialViewController.coreDataManager = coreDataManager
Last but not least, we set the rootViewController property of the window
property of the application delegate.
AppDelegate.swift
1 // Configure Window
2 window?.rootViewController = initialViewController
AppDelegate.swift
1 func application(_ application: UIApplication, didFinishLaunchingWit\
2 hOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bo\
3 ol {
4 // Load Storyboard
5 let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
6
7 // Instantiate Initial View Controller
8 guard let initialViewController = storyboard.instantiateInitialV\
9 iewController() as? ViewController else {
10 fatalError("Unable to Configure Initial View Controller")
11 }
12
13 // Configure Initial View Controller
14 initialViewController.coreDataManager = coreDataManager
15
16 // Configure Window
17 window?.rootViewController = initialViewController
18
19 return true
20 }
ViewController.swift
1 override func viewDidLoad() {
2 super.viewDidLoad()
3
4 print(coreDataManager?.managedObjectContext ?? "No Managed Objec\
5 t Context")
6 }
Before we move on, I’d like to move the instantiation of the Core Data
manager to the view controller. This makes more sense. The application
delegate shouldn’t be bothered with anything related to Core Data.
ViewController.swift
1 import UIKit
2
3 class ViewController: UIViewController {
4
5 // MARK: - Properties
6
7 private var coreDataManager = CoreDataManager(modelName: "Notes")
8
9 // MARK: - View Life Cycle
10
11 override func viewDidLoad() {
12 super.viewDidLoad()
13 }
14
15 // MARK: - Navigation
16
17 override func prepare(for segue: UIStoryboardSegue, sender: Any?\
18 ) {
19
20 }
21
22 }
AppDelegate.swift
1 import UIKit
2
3 @UIApplicationMain
4 class AppDelegate: UIResponder, UIApplicationDelegate {
5
6 // MARK: - Properties
7
8 var window: UIWindow?
9
10 // MARK: - Application Life Cycle
11
12 func application(_ application: UIApplication, didFinishLaunchin\
13 gWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -\
14 > Bool {
15 return true
16 }
17
18 }
CoreDataManager.swift
1 private lazy var managedObjectModel: NSManagedObjectModel = {
2 // Fetch Model URL
3 guard let modelURL = Bundle.main.url(forResource: self.modelName\
4 , withExtension: "momd") else {
5 fatalError("Unable to Find Data Model")
6 }
7
8 // Initialize Managed Object Model
9 guard let managedObjectModel = NSManagedObjectModel(contentsOf: \
10 modelURL) else {
11 fatalError("Unable to Load Data Model")
12 }
13
14 return managedObjectModel
15 }()
The file that’s loaded from the application bundle is named Notes and has an
momd extension. The extension stands for managed object model
document. As we learned earlier, the file extension of the data model in the
Project Navigator is different, xcdatamodeld. And yet if you run the
application, the Core Data stack is set up without issues. How’s that possible?
When the application is built, the data model file you see in the Project
Navigator is compiled into an momd file. Why is that necessary? Why does
the data model need to be compiled?
Compiling the Data Model
The xcdatamodeld file is the file we edit during development. We use it for
defining the application’s data model. It shows us a visual representation of the
data model as we’ll see in a moment. Much of the information in the
xcdatamodeld file isn’t needed for Core Data to do its work.
At compile time, Xcode collects the data it needs from the xcdatamodeld file
and creates an momd file. It’s the momd file that’s included in the compiled
application. The resulting momd file is much smaller and it only contains
what’s absolutely essential for Core Data to infer the data model.
The data model editor has two styles, table and graph. You can toggle
between these styles with the control in the lower right of the data model
editor.
Toggling Between Editor Styles
The table style is useful for adding and editing entities, attributes, and
relationships. Most of your time is spent in the table style. The graph style is
ideal for visualizing relationships between entities. But we first need to answer
the question “What is an entity?”
What Is an Entity
Even though Core Data isn’t a database, you can think of an entity as a table
in a database. An entity has a name and properties. A property is either an
attribute or a relationship. For example, an entity named Person can have
an attribute firstName and lastName. It could also have a relationship
address that points to an Address entity. We discuss relationships in more
detail in the next chapter.
What Is an Entity
Creating an Entity
You can add an entity by clicking the Add Entity button at the bottom of the
data model editor. The editor’s table style is split into a navigator on the left
and a detail view on the right. In the Entities section of the navigator, you
should see an entity named Entity.
Adding an Entity
Select the entity, open the Utilities pane on the right, and select the Data
Model Inspector. The inspector shows the details of the entity. Set the name
of the entity to Note.
Editing an Entity
Creating an Attribute
With the Note entity selected, click the Add Attribute button at the bottom to
add an attribute to the entity. In the Attributes table, set Attribute to title and
Type to String. You can inspect the details of an attribute by selecting it and
opening the Data Model Inspector in the Utilities pane on the right.
Adding an Attribute
In the Data Model Inspector, you can see that an attribute has a number of
configuration options, such as validation rules, a default value, and indexing
options. Some of the options differ from type to type.
The checkbox Optional is checked by default. This indicates that the attribute
title is optional for the Note entity. What does that mean?
If we create a note and the value of the title attribute isn’t set, Core Data won’t
complain because the attribute is marked as optional. It’s fine if the note record
doesn’t have a title. If you make the attribute required by unchecking the
checkbox, Core Data throws an error if you try to save a note that doesn’t have
a title.
Requiring Attributes
Create another entity and name it Tag. This entity also has one attribute,
name of type String. Select the name attribute and uncheck Optional in the
Data Model Inspector on the right.
Adding the Tag Entity
Defining Relationships
Before we start defining relationships, we need to take a moment to talk about
the relationships between the entities we defined. A note can only belong to
one category. But a category can have multiple notes. A note can have
multiple tags and a tag can belong to multiple notes.
When working with data models, it’s important to spend some time planning
and reflecting what the data model should look like. I cannot emphasize this
enough. Once you’ve decided on the data model, it’s often expensive to make
drastic changes to the data model. We’ll learn more about what that means for
Core Data when we discuss data model migrations.
Switch to the editor’s graph style to see the visual representation of the
relationship we defined.
Before we move on, there are a number of details we need to discuss. The
relationship we added is a to-one relationship, a note can belong to only one
category. This is visualized in the data model graph by the single arrow
pointing from the Note entity to the Category entity.
Notice that the arrow points from Note to Category. There is no arrow pointing
back from Category to Note. There is no inverse relationship because we
haven’t defined one yet. This implies that the category the note belongs to
doesn’t know that the note belongs to the category. That’s not what we want,
though. A category should know what notes it contains. Right?
Switch to the data model editor’s graph style to see what that looks like. The
connection between Category and Note contains arrows pointing to and from
each entity.
Switch back to the data model editor’s table style and select the Note entity.
Notice that the inverse relationship of the category relationship is
automatically set to notes because we set the inverse relationship of the
notes relationship to category. Core Data is clever enough to take care of this
small but important detail for us.
Creating an Inverse Relationship
Select the notes relationship of the Category entity in the data model editor’s
table style. Open the Data Model Inspector in the Utilities pane on the right
to see the details of the relationship.
In the Data Model Inspector, you can modify the relationship’s destination
and inverse relationship. You can also modify the relationship’s type or
cardinality. Core Data supports To-One and To-Many relationships. Set Type
to To-Many.
Modifying the Relationship Type
Switch back to the data model editor’s graph style to see what has changed.
The relationship from Category to Note now has two arrows, indicating that
notes is a To-Many relationship.
This is what the data model graph now looks like. The relationship that links
the Note and Tag entities is a Many-To-Many relationship.
Creating a Many-To-Many Relationship
In the next chapter, we further discuss relationships and how we can configure
relationships in the data model editor.
9 Configuring Relationships
Core Data is much more than a database and this becomes clear when you
start working with relationships. Relationships in Core Data are powerful
because the framework does a lot of the heavy lifting for us.
Delete Rules
Delete rules are one of the conveniences that make working with Core Data
great. Every relationship has a delete rule. It defines what happens when the
record that owns the relationship is deleted. Let me explain what that means.
Select the notes relationship of the Category entity and open the Data Model
Inspector on the right.
- No Action
- Nullify
- Cascade
- Deny
No Action
If the delete rule of a relationship is set to No Action, nothing happens when
the owner of the relationship is deleted. Let me illustrate this with an example.
We have a category record that contains several note records. If the category
record is deleted, the note records are not notified of this event. Every note
record thinks it’s still associated with the deleted category.
I’ve never had a need to use this delete rule. In most situations, you want to
take some action when a record is deleted. And that’s where the other delete
rules come into play.
Nullify
If the delete rule of a relationship is set to Nullify, the destination of the
relationship is nullified when the owner of the relationship is deleted. If a
category has several notes and the category is deleted, the relationship
pointing from the notes to the category is nullified. This is the default delete
rule and the delete rule you will find yourself using most often.
Cascade
Cascade is useful if the data model includes one or more dependencies. For
example, if an account record has a relationship to one or more user records, it
may be desirable to delete every user if the account the user belongs to is
deleted. In other words, the deletion of the account record cascades or trickles
down to the user records it is linked to.
Cascade Delete Rule
Deny
Deny is another powerful and useful pattern. It’s the opposite of the Cascade
delete rule. Instead of cascading the deletion of a record, it prevents the
deletion of a record.
For example, if an account is associated with several users, the account can
only be deleted if it’s no longer tied to any users. This configuration prevents
the scenario in which users are no longer associated with an account.
The inverse is also true. If the user deletes a category, should the application
automatically delete every note linked to that category? No. That’s not the
behavior the user expects. Nullify is the delete rule we stick with.
The same logic applies to the relationships of the Note and Tag entities.
Deleting a note or a tag shouldn’t result in the deletion of any records
associated with the deleted record. We stick with Nullify for both relationships.
Another Scenario
But this isn’t the only possible configuration. Let’s assume a note must have a
category. This means we mark the category relationship of the Note entity as
required by unchecking the Optional checkbox in the Data Model Inspector.
What’s different? What would or should happen if a category is deleted?
Nothing changes if the user deletes a note. The delete rule of the category
relationship should be set to Nullify. But what happens if the user deletes a
category and the notes relationship is set to Nullify? Every note is required to
be linked to a category. To avoid that notes end up without a category, we
could set the delete rule to Deny to make sure a category with one or more
notes cannot be deleted.
While this is a possible option, it would most likely frustrate the user because it
would result in an unpleasant and odd user experience. We could offer the
user the option to delete every note linked to the category when the category is
deleted, but that too would be an odd workaround.
More Configuration
If you take a closer look at the Data Model Inspector, you can see that Xcode
offers us a few more options to configure relationships. We can, for example,
define validation rules. We could, for example, define a minimum and
maximum number of tags for a note record.
Configuring Relationships
ViewController.swift
1 import CoreData
- an entity description
- a managed object context
Entity Description
Every managed object has an entity description, an instance of the
NSEntityDescription class. The entity description is accessible through the
entity property of the managed object.
Earlier in this book, you learned what an entity is and we created several
entities in the data model. An instance of the NSEntityDescription class
represents an entity of the data model. As the name of the class implies, an
NSEntityDescription instance describes an entity.
The entity description refers to a specific entity in the data model and it knows
about the attributes and relationships of that entity. Every managed object is
associated with an entity description.
ViewController.swift
1 override func viewDidLoad() {
2 super.viewDidLoad()
3
4 if let entityDescription = NSEntityDescription.entity(forEntityN\
5 ame: "Note", in: coreDataManager.managedObjectContext) {
6
7 }
8 }
We should now have a valid entity description. Let’s print the name and the
properties of the entity description to the console.
ViewController.swift
1 override func viewDidLoad() {
2 super.viewDidLoad()
3
4 if let entityDescription = NSEntityDescription.entity(forEntityN\
5 ame: "Note", in: coreDataManager.managedObjectContext) {
6 print(entityDescription.name ?? "No Name")
7 print(entityDescription.properties)
8 }
9 }
Run the application and inspect the output in the console. Oh. It seems the
application crashed.
The persistent store coordinator was unable to add the persistent store. If you
encounter the same crash, remove the application from the device or simulator
and run the application again. We explore the reason of this crash later in this
book. This is what the output in the console looks like.
Console
1 Note
2 [(<NSAttributeDescription: 0x6000000ed980>), name contents, isOption\
3 al 1, isTransient 0, entity Note, renamingIdentifier contents, valid\
4 ation predicates (
5 ), warnings (
6 ), versionHashModifier (null)
7 userInfo {
8 }, attributeType 700 , attributeValueClassName NSString, defaultValu\
9 e (null), (<NSAttributeDescription: 0x6000000eda80>), name createdAt\
10 , isOptional 1, isTransient 0, entity Note, renamingIdentifier creat\
11 edAt, validation predicates (
12 ), warnings (
13 ), versionHashModifier (null)
14 userInfo {
15 }, attributeType 900 , attributeValueClassName NSDate, defaultValue \
16 (null), (<NSAttributeDescription: 0x6000000ed700>), name title, isOp\
17 tional 1, isTransient 0, entity Note, renamingIdentifier title, vali\
18 dation predicates (
19 ), warnings (
20 ), versionHashModifier (null)
21 userInfo {
22 }, attributeType 700 , attributeValueClassName NSString, defaultValu\
23 e (null), (<NSAttributeDescription: 0x6000000ed900>), name updatedAt\
24 , isOptional 1, isTransient 0, entity Note, renamingIdentifier updat\
25 edAt, validation predicates (
26 ), warnings (
27 ), versionHashModifier (null)
28 userInfo {
29 }, attributeType 900 , attributeValueClassName NSDate, defaultValue \
30 (null), (<NSRelationshipDescription: 0x600000130ea0>), name category\
31 , isOptional 1, isTransient 0, entity Note, renamingIdentifier categ\
32 ory, validation predicates (
33 ), warnings (
34 ), versionHashModifier (null)
35 userInfo {
36 }, destination entity Category, inverseRelationship notes, minCount \
37 0, maxCount 1, isOrdered 0, deleteRule 1, (<NSRelationshipDescriptio\
38 n: 0x600000130e00>), name tags, isOptional 1, isTransient 0, entity \
39 Note, renamingIdentifier tags, validation predicates (
40 ), warnings (
41 ), versionHashModifier (null)
42 userInfo {
43 }, destination entity Tag, inverseRelationship notes, minCount 0, ma\
44 xCount 0, isOrdered 0, deleteRule 1]
That looks like a lot of gibberish. If you take a closer look, though, it makes
more sense. The first line tells us the name of the entity is Note. We can also
see that the entity has four attributes and two relationships.
ViewController.swift
1 // Initialize Managed Object
2 let note = NSManagedObject(entity: entityDescription, insertInto: co\
3 reDataManager.managedObjectContext)
The entity description and managed object context are both available as
properties on the managed object. Add a print statement for the managed
object, run the application again, and inspect the output in the console.
ViewController.swift
1 override func viewDidLoad() {
2 super.viewDidLoad()
3
4 if let entityDescription = NSEntityDescription.entity(forEntityN\
5 ame: "Note", in: coreDataManager.managedObjectContext) {
6 print(entityDescription.name ?? "No Name")
7 print(entityDescription.properties)
8
9 // Initialize Managed Object
10 let note = NSManagedObject(entity: entityDescription, insert\
11 Into: coreDataManager.managedObjectContext)
12
13 print(note)
14 }
15 }
1 <Note: 0x6080000954a0> (entity: Note; id: 0x608000221440 <x-coredata\
2 :///Note/tE5C54D25-1766-4940-A0E5-F9361FA778052> ; data: {
3 category = nil;
4 contents = nil;
5 createdAt = nil;
6 tags = (
7 );
8 title = nil;
9 updatedAt = nil;
10 })
The output shows us that the managed object we created doesn’t have values
for any of its attributes or relationships. It also tells us that no tag or category
records are associated with the note record.
ViewController.swift
1 // Initialize Managed Object
2 let note = NSManagedObject(entity: entityDescription, insertInto: co\
3 reDataManager.managedObjectContext)
4
5 // Configure Managed Object
6 note.setValue("My First Note", forKey: "title")
7 note.setValue(NSDate(), forKey: "createdAt")
8 note.setValue(NSDate(), forKey: "updatedAt")
If we run the application again and print the value of note to the console, we
see that the managed object contains the data we set.
1 <Note: 0x61800009f9a0> (entity: Note; id: 0x61800003afa0 <x-coredata\
2 :///Note/t6B25F688-928B-4DDF-BE67-564B8FBE0DCB2> ; data: {
3 category = nil;
4 contents = nil;
5 createdAt = "2017-07-05 12:25:59 +0000";
6 tags = (
7 );
8 title = "My First Note";
9 updatedAt = "2017-07-05 12:25:59 +0000";
10 })
To push the managed object to the persistent store, we need to save the
managed object context. Remember that a managed object context is a
workspace that allows us to work with managed objects. Any changes we
make to the managed object are only pushed to the persistent store if we save
the managed object context the managed object belongs to. We do this by
invoking save() on the managed object context.
ViewController.swift
1 // Initialize Managed Object
2 let note = NSManagedObject(entity: entityDescription, insertInto: co\
3 reDataManager.managedObjectContext)
4
5 // Configure Managed Object
6 note.setValue("My First Note", forKey: "title")
7 note.setValue(NSDate(), forKey: "createdAt")
8 note.setValue(NSDate(), forKey: "updatedAt")
9
10 print(note)
11
12 do {
13 try coreDataManager.managedObjectContext.save()
14 } catch {
15 print("Unable to Save Managed Object Context")
16 print("\(error), \(error.localizedDescription)")
17 }
We can inspect the persistent store to verify that the save operation was
successful. I use SimPholders to make this easier. It allows me to quickly
inspect the sandbox of any application installed in the simulator.
Even though we only created a note record in this chapter, we learned a lot
about how Core Data works under the hood. Knowing this is important for
debugging problems you encounter along the way. And believe me, you will
run into problems at some point. If you understand the fundamentals of the
framework, you’re in a much better position to solve any issues that arise. In
the next chapter, we continue working with managed objects.
11 Subclassing NSManagedObject
In the previous chapter, we used the NSManagedObject class to represent and
interact with records stored in the persistent store. This works fine, but the
syntax is verbose, we can’t take advantage of Xcode’s autocompletion, and
type safety is also an issue.
If we want to access the title of the note record, for example, we need to
invoke value(forKey:) on the record and cast the result to a String object.
1 if let title = note.value(forKey: "title") as? String {
2 print(title)
3 }
This isn’t pretty and it gets old very quickly. Fortunately, there’s a solution,
subclassing NSManagedObject.
Code Generation
Prior to Xcode 8, developers needed to manually create subclasses for each
entity. This is no longer necessary. As of Xcode 8, there’s a much cleaner
solution. Revisit the data model and select the Note entity. Open the Data
Model Inspector on the right and take a look at the Class section. The
Codegen field is the setting we’re interested in. As of Xcode 8.1, this is set by
default to Class Definition.
Convenience Methods
Because the Note class knows what entity it is linked to, the initializer no longer
requires an NSEntityDescription instance. We only need to specify the
managed object context for the managed object.
1 // Initialize Note
2 let note = Note(context: coreDataManager.managedObjectContext)
Populating the note record is concise, type safe, and we benefit from Xcode’s
autocompletion. This is a significant improvement.
1 // Configure Note
2 note.title = "My Second Note"
3 note.createdAt = Date()
4 note.updatedAt = Date()
But notice that the properties of the Note class are optionals. Why is that?
Properties Are Optionals
It’s important to understand that the optionality of the properties has nothing to
do with the Optional checkbox in the Data Model Inspector. These are
separate issues.
Why are the properties optionals? When a managed object is created, the
value of each property is set to nil. This is only possible if the properties are
optionals. It’s that simple.
I agree that this is unfortunate, but that’s the current state of Core Data. The
framework started its life as an Objective-C framework and this is a side effect
when it’s used in combination with Swift.
What to Remember
What I want you to remember from this chapter is that an NSManagedObject
subclass is automatically created for every entity as of Xcode 8.1 and that the
optionality of the properties of an NSManagedObject subclass isn’t linked to the
Optional checkbox in the Data Model Editor.
We now know enough to continue building Notes. In the next chapters, we add
the ability to create, read, update, and delete notes.
12 Adding a Note
In the next chapters, we add the ability to create, read, update, and delete
notes. Before we start, we need to make some preparations.
Target Configuration
Open the project in the Project Navigator, choose the Notes target, and
select the General tab at the top. Set Devices to iPhone, Device Orientation
to Portrait, and check Requires full screen.
View Controllers
I’m not going to bother you with the implementation of the user interface and
the view controllers we need. We’re going to focus on the details that relate to
the Core Data implementation. Open the starter project of this chapter if you’d
like to follow along with me.
With this in place, what changes do we need to make to add the ability to
create notes?
Adding Notes
The AddNoteViewController class can only create a note if it has access to a
managed object context. This means we need to pass it a reference to the
Core Data manager’s managed object context.
AddNoteViewController.swift
1 import UIKit
2 import CoreData
3
4 class AddNoteViewController: UIViewController {
5
6 // MARK: - Properties
7
8 @IBOutlet var titleTextField: UITextField!
9 @IBOutlet var contentsTextView: UITextView!
10
11 // MARK: -
12
13 var managedObjectContext: NSManagedObjectContext?
14
15 ...
16
17 }
NotesViewController.swift
1 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
2 guard let identifier = segue.identifier else { return }
3
4 switch identifier {
5 case Segue.AddNote:
6 guard let destination = segue.destination as? AddNoteViewCon\
7 troller else {
8 return
9 }
10
11 // Configure Destination
12 destination.managedObjectContext = coreDataManager.managedOb\
13 jectContext
14 default:
15 break
16 }
17 }
The next step we need to take is creating and populating a note when the user
taps the Save button. We create a note in the save(sender:) action.
AddNoteViewController.swift
1 @IBAction func save(sender: UIBarButtonItem) {
2
3 }
AddNoteViewController.swift
1 guard let managedObjectContext = managedObjectContext else { return }
2 guard let title = titleTextField.text, !title.isEmpty else {
3 showAlert(with: "Title Missing", and: "Your note doesn't have a \
4 title.")
5 return
6 }
If the title text field is empty we show an alert to the user by invoking a helper
method, showAlert(with:and:). This method is implemented in an extension
for UIViewController.
UIViewController.swift
1 import UIKit
2
3 extension UIViewController {
4
5 // MARK: - Alerts
6
7 func showAlert(with title: String, and message: String) {
8 // Initialize Alert Controller
9 let alertController = UIAlertController(title: title, messag\
10 e: message, preferredStyle: .alert)
11
12 // Configure Alert Controller
13 alertController.addAction(UIAlertAction(title: "OK", style: \
14 .default, handler: nil))
15
16 // Present Alert Controller
17 present(alertController, animated: true, completion: nil)
18 }
19
20 }
Using the managed object context, we create a Note instance and populate it
with the data the user has entered.
AddNoteViewController.swift
1 // Create Note
2 let note = Note(context: managedObjectContext)
3
4 // Configure Note
5 note.createdAt = Date()
6 note.updatedAt = Date()
7 note.title = titleTextField.text
8 note.contents = contentsTextView.text
We pop the add note view controller from the navigation stack to return to the
notes view controller.
AddNoteViewController.swift
1 // Pop View Controller
2 _ = navigationController?.popViewController(animated: true)
That’s it. That’s all it takes to create a note. This is the implementation of the
save(sender:) method.
AddNoteViewController.swift
1 @IBAction func save(sender: UIBarButtonItem) {
2 guard let managedObjectContext = managedObjectContext else { ret\
3 urn }
4 guard let title = titleTextField.text, !title.isEmpty else {
5 showAlert(with: "Title Missing", and: "Your note doesn't hav\
6 e a title.")
7 return
8 }
9
10 // Create Note
11 let note = Note(context: managedObjectContext)
12
13 // Configure Note
14 note.createdAt = Date()
15 note.updatedAt = Date()
16 note.title = titleTextField.text
17 note.contents = contentsTextView.text
18
19 // Pop View Controller
20 _ = navigationController?.popViewController(animated: true)
21 }
Before you run the application, make sure you delete the application first. We
want to start with a clean slate. Tap the bar button item, fill out the title text
field and the contents text view, and tap Save.
Because the notes view controller doesn’t display the list of notes yet, we can’t
verify that everything is working. We’ll fix that later.
CoreDataManager.swift
1 init(modelName: String) {
2 self.modelName = modelName
3
4 setupNotificationHandling()
5 }
- UIApplicationWillTerminate
- UIApplicationDidEnterBackground
CoreDataManager.swift
1 private func setupNotificationHandling() {
2 let notificationCenter = NotificationCenter.default
3 notificationCenter.addObserver(self,
4 selector: #selector(saveChanges(_\
5 :)),
6 name: Notification.Name.UIApplica\
7 tionWillTerminate,
8 object: nil)
9
10 notificationCenter.addObserver(self,
11 selector: #selector(saveChanges(_\
12 :)),
13 name: Notification.Name.UIApplica\
14 tionDidEnterBackground,
15 object: nil)
16 }
When the Core Data manager receives one of these notifications, the
saveChanges(_:) method is invoked. In this method, we invoke another helper
method, saveChanges().
CoreDataManager.swift
1 @objc func saveChanges(_ notification: Notification) {
2 saveChanges()
3 }
Saving Changes
The save operation takes place in the saveChanges() method.
CoreDataManager.swift
1 private func saveChanges() {
2
3 }
We first ask the managed object context if it has changes we need to push to
the persistent store. We do this by asking for the value of its hasChanges
property. We exit early if no changes need to be pushed to the persistent
store.
CoreDataManager.swift
1 guard managedObjectContext.hasChanges else { return }
If the managed object context has changes we need to push, we invoke save()
on the managed object context in a do-catch statement. Remember that
save() is a throwing method. If the save operation fails, we print the error to
the console.
CoreDataManager.swift
1 do {
2 try managedObjectContext.save()
3 } catch {
4 print("Unable to Save Managed Object Context")
5 print("\(error), \(error.localizedDescription)")
6 }
This is what the implementation of saveChanges() looks like.
CoreDataManager.swift
1 private func saveChanges() {
2 guard managedObjectContext.hasChanges else { return }
3
4 do {
5 try managedObjectContext.save()
6 } catch {
7 print("Unable to Save Managed Object Context")
8 print("\(error), \(error.localizedDescription)")
9 }
10 }
Because the save operation takes place when the application isn’t in the
foreground, it isn’t useful to notify the user if the save operation failed.
However, that doesn’t mean that you can ignore any errors that are thrown
when something goes wrong. It’s recommended to notify the user at some
point that a problem occurred.
That seems to work fine. In the next chapter, you learn how to fetch the notes
from the persistent store and display them in the notes view controller. That’s
going to be an important and interesting chapter.
14 Fetch Those Notes
In this chapter, we fetch the user’s notes from the persistent store and display
them in a table view. The notes view controller is in charge of these tasks.
Before We Start
I’ve already updated the storyboard with a basic user interface. Let me walk
you through it. The view of the notes view controller contains a label, for
displaying a message to the user, and a table view, for listing the user’s notes.
The label and the table view are embedded in another view, the notes view.
The reason for this becomes clear later in this book. Don’t worry about it for
now.
The table view has one prototype cell of type NoteTableViewCell. The
NoteTableViewCell class defines three outlets, a label for the title of the note, a
label that displays the time and date when the note was last updated, and a
label for displaying a preview of the note’s contents.
NoteTableViewCell.swift
1 import UIKit
2
3 class NoteTableViewCell: UITableViewCell {
4
5 // MARK: - Static Properties
6
7 static let reuseIdentifier = "NoteTableViewCell"
8
9 // MARK: - Properties
10
11 @IBOutlet var titleLabel: UILabel!
12 @IBOutlet var contentsLabel: UILabel!
13 @IBOutlet var updatedAtLabel: UILabel!
14
15 // MARK: - Initialization
16
17 override func awakeFromNib() {
18 super.awakeFromNib()
19 }
20
21 }
As you may have guessed, the notes view controller is the delegate and data
source of the table view.
If we run the application, we see a message that tells us we don’t have any
notes yet despite the fact we successfully created a note in the previous
chapter. Let’s fix that.
Fetching Notes
To display the user’s notes, we first need to fetch them from the persistent
store. Open NotesViewController.swift and add an import statement for the
Core Data framework.
NotesViewController.swift
1 import UIKit
2 import CoreData
We declare a property for storing the notes we’re going to fetch from the
persistent store. We name the property notes and it should be of type [Note]?.
We also define a property observer because we want to update the user
interface every time the value of the notes property changes. In the property
observer, we invoke a helper method, updateView().
NotesViewController.swift
1 private var notes: [Note]? {
2 didSet {
3 updateView()
4 }
5 }
NotesViewController.swift
1 override func viewDidLoad() {
2 super.viewDidLoad()
3
4 title = "Notes"
5
6 setupView()
7
8 fetchNotes()
9 }
In the fetchNotes() method, we fetch the user’s notes from the persistent
store.
NotesViewController.swift
1 private func fetchNotes() {
2
3 }
NotesViewController.swift
1 // Create Fetch Request
2 let fetchRequest: NSFetchRequest<Note> = Note.fetchRequest()
Notice that we specify the type we expect from the fetch request. The compiler
takes care of the nitty-gritty details for us.
We want to sort the notes based on the value of the updatedAt property. In
other words, we want to show the most recently updated note at the top of the
table view. For that to work, we need to tell the fetch request how it should sort
the results it receives from the persistent store.
We create a sort descriptor, an instance of the NSSortDescriptor class, and set
the sortDescriptors property of the fetch request. The sortDescriptors
property is an array, which means we could specify multiple sort descriptors.
The sort descriptors are evaluated based on the order in which they appear in
the array.
NotesViewController.swift
1 // Configure Fetch Request
2 fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Note.\
3 updatedAt), ascending: false)]
Remember that we never directly access the persistent store. We execute the
fetch request using the managed object context of the Core Data manager. We
wrap the code for executing the fetch request in a closure, which is the
argument of the performAndWait(_:) method of the NSManagedObjectContext
class. What’s going on here?
NotesViewController.swift
1 // Perform Fetch Request
2 coreDataManager.managedObjectContext.performAndWait {
3
4 }
We take a closer look at the reasons for doing this later in this book. What you
need to remember for now is that by invoking the fetch request in the closure
of the performAndWait(_:) method, we access the managed object context on
the thread it’s associated with. Don’t worry if that makes no sense yet. It will
click once we discuss threading much later in this book.
NotesViewController.swift
1 // Perform Fetch Request
2 coreDataManager.managedObjectContext.performAndWait {
3 do {
4 // Execute Fetch Request
5 let notes = try fetchRequest.execute()
6
7 // Update Notes
8 self.notes = notes
9
10 // Reload Table View
11 self.tableView.reloadData()
12
13 } catch {
14 let fetchError = error as NSError
15 print("Unable to Execute Fetch Request")
16 print("\(fetchError), \(fetchError.localizedDescription)")
17 }
18 }
NotesViewController.swift
1 private func fetchNotes() {
2 // Create Fetch Request
3 let fetchRequest: NSFetchRequest<Note> = Note.fetchRequest()
4
5 // Configure Fetch Request
6 fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(N\
7 ote.updatedAt), ascending: false)]
8
9 // Perform Fetch Request
10 coreDataManager.managedObjectContext.performAndWait {
11 do {
12 // Execute Fetch Request
13 let notes = try fetchRequest.execute()
14
15 // Update Notes
16 self.notes = notes
17
18 // Reload Table View
19 self.tableView.reloadData()
20
21 } catch {
22 let fetchError = error as NSError
23 print("Unable to Execute Fetch Request")
24 print("\(fetchError), \(fetchError.localizedDescription)\
25 ")
26 }
27 }
28 }
That was probably one of the most complicated sections of this book. Make
sure you understand the what and why of the fetch request. Read this chapter
again if necessary because it’s important that you understand what’s going on.
Remember that you can ignore the performAndWait(_:) method for now, but
make sure you understand how to create and execute a fetch request.
Displaying Notes
Before we move on, I want to implement a computed property that tells us if
we have notes to display. The implementation of the hasNotes property is pretty
straightforward.
NotesViewController.swift
1 private var hasNotes: Bool {
2 guard let notes = notes else { return false }
3 return notes.count > 0
4 }
NotesViewController.swift
1 private func updateView() {
2 tableView.isHidden = !hasNotes
3 messageLabel.isHidden = hasNotes
4 }
NotesViewController.swift
1 func numberOfSections(in tableView: UITableView) -> Int {
2 return hasNotes ? 1 : 0
3 }
NotesViewController.swift
1 func tableView(_ tableView: UITableView, numberOfRowsInSection secti\
2 on: Int) -> Int {
3 guard let notes = notes else { return 0 }
4 return notes.count
5 }
But it seems that we have a problem. Because we’re dealing with Core Data,
the type of the updatedAt property is NSDate?. The date formatter we use to
convert the date to a string doesn’t like that.
The solution is simple. We create an extension for the Note class and define
computed properties for the updatedAt and createdAt properties, which return a
Date instance. Create a new group, Extensions, in the Core Data group and
add a file named Note.swift. We import Foundation and create an extension
for the Note class. The implementation of the computed properties is
straightforward.
Note.swift
1 import Foundation
2
3 extension Note {
4
5 var updatedAtAsDate: Date {
6 guard let updatedAt = updatedAt else { return Date() }
7 return Date(timeIntervalSince1970: updatedAt.timeIntervalSin\
8 ce1970)
9 }
10
11 var createdAtAsDate: Date {
12 guard let createdAt = createdAt else { return Date() }
13 return Date(timeIntervalSince1970: createdAt.timeIntervalSin\
14 ce1970)
15 }
16
17 }
NotesViewController.swift
1 func tableView(_ tableView: UITableView, cellForRowAt indexPath: Ind\
2 exPath) -> UITableViewCell {
3 // Fetch Note
4 guard let note = notes?[indexPath.row] else {
5 fatalError("Unexpected Index Path")
6 }
7
8 // Dequeue Reusable Cell
9 guard let cell = tableView.dequeueReusableCell(withIdentifier: N\
10 oteTableViewCell.reuseIdentifier, for: indexPath) as? NoteTableViewC\
11 ell else {
12 fatalError("Unexpected Index Path")
13 }
14
15 // Configure Cell
16 cell.titleLabel.text = note.title
17 cell.contentsLabel.text = note.contents
18 cell.updatedAtLabel.text = updatedAtDateFormatter.string(from: n\
19 ote.updatedAtAsDate)
20
21 return cell
22 }
And with that change, we’re ready to take the application for a spin. You
should now see the notes of the user displayed in the table view.
In this chapter, you learned how to fetch data from the persistent store. In the
next chapter, you learn how to update notes and automatically update the
table view.
15 Fix That Mistake
The application we’re building wouldn’t be very useful if it didn’t include the
ability to edit notes. Would it? If the user taps a note in the notes view
controller, they should be able to modify the title and contents of the note.
Before We Start
I’ve already created the NoteViewController class for this purpose. The note
view controller is responsible for updating notes. We could have used the
AddNoteViewController class for this, but I usually create separate view
controllers for adding and updating records. That’s merely a personal choice.
Passing a Note
The first step we need to take is pass the note the user wants to update to the
note view controller. Open NoteViewController.swift and declare a property,
note, of type Note?. We could use an implicitly unwrapped optional, but I prefer
to play it safe by using an optional. In general, I avoid implicitly unwrapped
optionals whenever possible with the exception of outlets.
NoteViewController.swift
1 import UIKit
2
3 class NoteViewController: UIViewController {
4
5 // MARK: - Properties
6
7 @IBOutlet var titleTextField: UITextField!
8 @IBOutlet var contentsTextView: UITextView!
9
10 // MARK: -
11
12 var note: Note?
13
14 ...
15
16 }
I hope it’s clear why we need to use an optional. Every stored property of a
class or struct needs to have a valid value by the time the instance is created.
This leaves us no option but to use an optional.
NotesViewController.swift
1 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
2 guard let identifier = segue.identifier else { return }
3
4 switch identifier {
5 case Segue.AddNote:
6 ...
7 case Segue.Note:
8 guard let destination = segue.destination as? NoteViewContro\
9 ller else {
10 return
11 }
12
13 guard let indexPath = tableView.indexPathForSelectedRow, let\
14 note = notes?[indexPath.row] else {
15 return
16 }
17
18 // Configure Destination
19 destination.note = note
20 default:
21 break
22 }
23 }
NoteViewController.swift
1 override func viewDidLoad() {
2 super.viewDidLoad()
3
4 title = "Edit Note"
5
6 setupView()
7 }
- setupTitleTextField()
- setupContentsTextView()
NoteViewController.swift
1 private func setupView() {
2 setupTitleTextField()
3 setupContentsTextView()
4 }
In these helper methods, we set the text field and the text view with the data of
the note.
NoteViewController.swift
1 private func setupTitleTextField() {
2 // Configure Title Text Field
3 titleTextField.text = note?.title
4 }
5
6 private func setupContentsTextView() {
7 // Configure Contents Text View
8 contentsTextView.text = note?.contents
9 }
Updating a Note
Updating the note is easy. We don’t even need a save button. We simply
update the note in the viewWillDisappear(_:) method of the note view
controller. Not having a save button is a very nice feature from a user’s
perspective. The user has the impression that every change they make is
automatically saved. And that’s what happens behind the scenes.
NoteViewController.swift
1 override func viewWillDisappear(_ animated: Bool) {
2 super.viewWillDisappear(animated)
3
4 // Update Note
5 if let title = titleTextField.text, !title.isEmpty {
6 note?.title = title
7 }
8
9 note?.updatedAt = Date()
10 note?.contents = contentsTextView.text
11 }
Notice that we check if the title text field isn’t empty before updating the note
record. We need to make sure the note has a title because the title property
is required. We also update the values of the updatedAt and contents
properties.
There are several options to solve this. We could perform a fetch request
every time the notes view controller is the active view controller but that’s a
waste of resources. There’s a much better solution that leverages the Core
Data framework. Notifications.
- NSManagedObjectContextWillSave
- NSManagedObjectContextDidSave
- NSManagedObjectContextObjectsDidChange
NotesViewController.swift
1 override func viewDidLoad() {
2 super.viewDidLoad()
3
4 title = "Notes"
5
6 setupView()
7 fetchNotes()
8 setupNotificationHandling()
9 }
In this helper method, we add the notes view controller as an observer of the
NSManagedObjectContextObjectsDidChange notification.
NotesViewController.swift
1 private func setupNotificationHandling() {
2 let notificationCenter = NotificationCenter.default
3 notificationCenter.addObserver(self,
4 selector: #selector(managedObject\
5 ContextObjectsDidChange(_:)),
6 name: NSNotification.Name.NSManag\
7 edObjectContextObjectsDidChange,
8 object: coreDataManager.managedOb\
9 jectContext)
10 }
Notice that the last argument of the method is the managed object context of
the Core Data manager. If you’re developing an application that uses multiple
managed object contexts, you need to make sure you only observe the
managed object context the object is interested in. This is very important, not
only from a performance perspective, but also in the context of threading.
That’ll become clear later in this book. Remember for now that you should only
observe the managed object context the object is interested in.
NotesViewController.swift
1 // MARK: - Notification Handling
2
3 @objc private func managedObjectContextObjectsDidChange(_ notificati\
4 on: Notification) {
5
6 }
The object property of the notification is the managed object context that sent
the notification.
NotesViewController.swift
1 // Helpers
2 var notesDidChange = false
Next, we extract the managed objects from the userInfo dictionary. The keys
we’re interested in are:
- NSInsertedObjectsKey
- NSUpdatedObjectsKey
- NSDeletedObjectsKey
NotesViewController.swift
1 if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObj\
2 ect> {
3
4 }
5
6 if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObje\
7 ct> {
8
9 }
10
11 if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObje\
12 ct> {
13
14 }
NotesViewController.swift
1 if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObj\
2 ect> {
3 for insert in inserts {
4 if let note = insert as? Note {
5 notes?.append(note)
6 notesDidChange = true
7 }
8 }
9 }
We apply the same logic for updated managed objects. The only difference is
that we don’t add them to the array of notes because they’re already part of
the array. But we do update the notesDidChange variable because we need to
update the user interface of the notes view controller.
NotesViewController.swift
1 if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObje\
2 ct> {
3 for update in updates {
4 if let _ = update as? Note {
5 notesDidChange = true
6 }
7 }
8 }
The logic for deleted managed objects is similar to that for inserted managed
objects. Instead of inserting the managed object, we remove it from the array
of notes.
NotesViewController.swift
1 if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObje\
2 ct> {
3 for delete in deletes {
4 if let note = delete as? Note {
5 if let index = notes?.index(of: note) {
6 notes?.remove(at: index)
7 notesDidChange = true
8 }
9 }
10 }
11 }
We obtain the index of the note in the array of notes and use that index to
remove it from the array. Again, we set notesDidChange to true to notify the
notes view controller that the user interface needs to be updated.
NotesViewController.swift
1 // MARK: - Notification Handling
2
3 @objc private func managedObjectContextObjectsDidChange(_ notificati\
4 on: Notification) {
5 guard let userInfo = notification.userInfo else { return }
6
7 // Helpers
8 var notesDidChange = false
9
10 if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManage\
11 dObject> {
12 for insert in inserts {
13 if let note = insert as? Note {
14 notes?.append(note)
15 notesDidChange = true
16 }
17 }
18 }
19
20 if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManaged\
21 Object> {
22 for update in updates {
23 if let _ = update as? Note {
24 notesDidChange = true
25 }
26 }
27 }
28
29 if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManaged\
30 Object> {
31 for delete in deletes {
32 if let note = delete as? Note {
33 if let index = notes?.index(of: note) {
34 notes?.remove(at: index)
35 notesDidChange = true
36 }
37 }
38 }
39 }
40
41 if notesDidChange {
42 // Sort Notes
43 notes?.sort(by: { $0.updatedAtAsDate > $1.updatedAtAsDate })
44
45 // Update Table View
46 tableView.reloadData()
47
48 // Update View
49 updateView()
50 }
51 }
Run the application again to see the result. Any changes made to a note are
immediately visible in the table view. If we have multiple notes, the note that
was last updated appears at the top. In the next chapter, we add the ability to
delete notes. This is surprisingly easy.
16 To the Trash Can
The application currently supports creating, reading, and updating notes. But it
should also be possible to delete notes. The pieces we need to add the ability
to delete notes are already present in the notes view controller. In fact, we only
need to implement one additional method of the UITableViewDataSource
protocol to add support for deleting notes.
Deleting a Note
Open NotesViewController.swift and revisit the implementation of the
UITableViewDataSource protocol. The method we need to implement is
tableView(_:commit:forRowAt:).
NotesViewController.swift
1 func tableView(_ tableView: UITableView, commit editingStyle: UITabl\
2 eViewCellEditingStyle, forRowAt indexPath: IndexPath) {
3
4 }
NotesViewController.swift
1 guard editingStyle == .delete else { return }
We then fetch the note that corresponds with the value of the indexPath
parameter.
NotesViewController.swift
1 // Fetch Note
2 guard let note = notes?[indexPath.row] else { fatalError("Unexpected\
3 Index Path") }
To delete the managed object, we pass the note to the delete(_:) method of
the managed object context to which the note belongs.
NotesViewController.swift
1 // Delete Note
2 note.managedObjectContext?.delete(note)
The implementation also works if we use the managed object context of the
Core Data manager because it’s the same managed object context.
NotesViewController.swift
1 // Delete Note
2 coreDataManager.managedObjectContext.delete(note)
In the next few chapters, we refactor the notes view controller. Instead of
manually keeping track of the notes of the persistent store, we outsource this
to an instance of the NSFetchedResultsController class, another useful
component of the Core Data framework.
17 Introducing the Fetched Results Controller
The application now supports creating, reading, updating, and deleting notes.
And this works fine. But Core Data has another trick up its sleeve.
On iOS and macOS, data fetched from the persistent store is very often
displayed in a table or collection view. Because this is such a common pattern,
the Core Data framework includes a class that’s specialized in managing the
results of a fetch request and providing the data needed to populate a table or
collection view. This class is the NSFetchedResultsController class.
NotesViewController.swift
1 private lazy var fetchedResultsController: NSFetchedResultsControlle\
2 r<Note> = {
3
4 }()
NotesViewController.swift
1 // Create Fetch Request
2 let fetchRequest: NSFetchRequest<Note> = Note.fetchRequest()
NotesViewController.swift
1 // Configure Fetch Request
2 fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Note.\
3 updatedAt), ascending: false)]
- a fetch request
- a managed object context
- a key path for creating sections
- a cache name for optimizing performance
NotesViewController.swift
1 // Create Fetched Results Controller
2 let fetchedResultsController = NSFetchedResultsController(fetchReque\
3 st: fetchRequest,
4 managedObj\
5 ectContext: self.coreDataManager.managedObjectContext,
6 sectionNam\
7 eKeyPath: nil,
8 cacheName:\
9 nil)
The managed object context we pass to the initializer is the managed object
context that’s used to perform the fetch request. The key path and cache name
are not important for this discussion.
Before we return the fetched results controller from the closure, we set its
delegate to self, the view controller.
NotesViewController.swift
1 // Configure Fetched Results Controller
2 fetchedResultsController.delegate = self
NotesViewController.swift
1 private func fetchNotes() {
2 do {
3 try self.fetchedResultsController.performFetch()
4 } catch {
5 print("Unable to Perform Fetch Request")
6 print("\(error), \(error.localizedDescription)")
7 }
8 }
To make sure the user interface is updated after performing the fetch request
in viewDidLoad(), we invoke updateView() at the end of the viewDidLoad()
method.
NotesViewController.swift
1 override func viewDidLoad() {
2 super.viewDidLoad()
3
4 title = "Notes"
5
6 setupView()
7 fetchNotes()
8 setupNotificationHandling()
9
10 updateView()
11 }
Because the fetched results controller now manages the notes fetched from
the persistent store, we can get rid of the notes property and ask the fetched
results controller for the data we need.
This also means that the implementation of the hasNotes property needs some
changes. We ask the fetched results controller for the value of its
fetchedObjects property, the managed objects it fetched from the persistent
store.
NotesViewController.swift
1 private var hasNotes: Bool {
2 guard let fetchedObjects = fetchedResultsController.fetchedObjec\
3 ts else { return false }
4 return fetchedObjects.count > 0
5 }
If the value of fetchedObjects isn’t equal to nil, we return true if the number of
managed objects is greater than 0. As you can see, this isn’t rocket science.
NotesViewController.swift
1 func numberOfSections(in tableView: UITableView) -> Int {
2 guard let sections = fetchedResultsController.sections else { re\
3 turn 0 }
4 return sections.count
5 }
NotesViewController.swift
1 func tableView(_ tableView: UITableView, numberOfRowsInSection secti\
2 on: Int) -> Int {
3 guard let section = fetchedResultsController.sections?[section] \
4 else { return 0 }
5 return section.numberOfObjects
6 }
To fetch the note, we ask the fetched results controller for the managed object
that corresponds with the index path. Again, the fetched results controller
knows very well how to handle hierarchical data.
NotesViewController.swift
1 func tableView(_ tableView: UITableView, cellForRowAt indexPath: Ind\
2 exPath) -> UITableViewCell {
3 // Dequeue Reusable Cell
4 guard let cell = tableView.dequeueReusableCell(withIdentifier: N\
5 oteTableViewCell.reuseIdentifier, for: indexPath) as? NoteTableViewC\
6 ell else {
7 fatalError("Unexpected Index Path")
8 }
9
10 // Fetch Note
11 let note = fetchedResultsController.object(at: indexPath)
12
13 ...
14
15 return cell
16 }
NotesViewController.swift
1 func tableView(_ tableView: UITableView, commit editingStyle: UITabl\
2 eViewCellEditingStyle, forRowAt indexPath: IndexPath) {
3 guard editingStyle == .delete else { return }
4
5 // Fetch Note
6 let note = fetchedResultsController.object(at: indexPath)
7
8 // Delete Note
9 coreDataManager.managedObjectContext.delete(note)
10 }
NotesViewController.swift
1 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
2 guard let identifier = segue.identifier else { return }
3
4 switch identifier {
5 case Segue.AddNote:
6 ...
7 case Segue.Note:
8 guard let destination = segue.destination as? NoteViewContro\
9 ller else {
10 return
11 }
12
13 guard let indexPath = tableView.indexPathForSelectedRow else\
14 {
15 return
16 }
17
18 // Fetch Note
19 let note = fetchedResultsController.object(at: indexPath)
20
21 // Configure Destination
22 destination.note = note
23 default:
24 break
25 }
26 }
Third, the view controller is the delegate of the fetched results controller. This
means it needs to conform to the NSFetchedResultsControllerDelegate
protocol. To satisfy the compiler we create an empty extension for the
NotesViewController class in which it conforms to the
NSFetchedResultsControllerDelegate protocol. This isn’t a problem because
every method of the protocol is optional. The
NSFetchedResultsControllerDelegate protocol is an Objective-C protocol,
which support optional methods.
NotesViewController.swift
1 extension NotesViewController: NSFetchedResultsControllerDelegate {
2
3 }
Run the application to see the result. Everything seems to work fine … well …
more or less. If we create, update, or delete a note, the table view isn’t
updated. For that to work, we need to implement the
NSFetchedResultsControllerDelegate protocol. We do that in the next chapter.
18 Exploring the
NSFetchedResultsControllerDelegate Protocol
To update the table view, we listened for notifications sent by the managed
object context of the Core Data manager. This is a perfectly fine solution. But it
can be messy to sift through the managed objects contained in the userInfo
dictionary of the notification. In a complex Core Data application, the
NSManagedObjectContextObjectsDidChange notification is sent very frequently. It
includes every change of every managed object, even the ones we may not be
interested in. We need to make sure we only respond to the changes of the
managed objects we are interested in.
The most important benefit of using a fetched results controller is that the
fetched results controller takes care of observing the managed object context
it’s tied to and it only notifies its delegate when it’s appropriate to update the
user interface.
This is a very powerful concept. We hand the fetched results controller a fetch
request and the fetched results controller makes sure its delegate is only
notified when the results of that fetch request change.
- controllerWillChangeContent(_:)
- controllerDidChangeContent(_:)
NotesViewController.swift
1 func controllerWillChangeContent(_ controller: NSFetchedResultsContr\
2 oller<NSFetchRequestResult>) {
3 tableView.beginUpdates()
4 }
NotesViewController.swift
1 func controllerDidChangeContent(_ controller: NSFetchedResultsContro\
2 ller<NSFetchRequestResult>) {
3 tableView.endUpdates()
4
5 updateView()
6 }
NotesViewController.swift
1 func controller(_ controller: NSFetchedResultsController<NSFetchRequ\
2 estResult>, didChange anObject: Any, at indexPath: IndexPath?, for t\
3 ype: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
4
5 }
- insert
- delete
- update
- move
The move type shows once more that the NSFetchedResultsController class is
a perfect fit for table and collection views.
NotesViewController.swift
1 func controller(_ controller: NSFetchedResultsController<NSFetchRequ\
2 estResult>, didChange anObject: Any, at indexPath: IndexPath?, for t\
3 ype: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
4 switch (type) {
5 case .insert:
6
7 case .delete:
8
9 case .update:
10
11 case .move:
12
13 }
14 }
Inserts
Let’s start with inserts first. The method gives us the destination of the
managed object that was inserted. This destination is stored in the
newIndexPath parameter. We unwrap the value of newIndexPath and insert a
row at the correct index path. The rest is taken care of by the implementation
of the UITableViewDataSource protocol.
NotesViewController.swift
1 case .insert:
2 if let indexPath = newIndexPath {
3 tableView.insertRows(at: [indexPath], with: .fade)
4 }
Deletes
Deletes are just as easy. The index path of the deleted managed object is
stored in the indexPath parameter. We safely unwrap the value of indexPath
and delete the corresponding row from the table view.
NotesViewController.swift
1 case .delete:
2 if let indexPath = indexPath {
3 tableView.deleteRows(at: [indexPath], with: .fade)
4 }
Updates
For updates, we don’t need to make changes to the table view itself. But we
need to fetch the table view cell that corresponds with the updated managed
object and update its contents.
NotesViewController.swift
1 func configure(_ cell: NoteTableViewCell, at indexPath: IndexPath) {
2 // Fetch Note
3 let note = fetchedResultsController.object(at: indexPath)
4
5 // Configure Cell
6 cell.titleLabel.text = note.title
7 cell.contentsLabel.text = note.contents
8 cell.updatedAtLabel.text = updatedAtDateFormatter.string(from: n\
9 ote.updatedAtAsDate)
10 }
NotesViewController.swift
1 func tableView(_ tableView: UITableView, cellForRowAt indexPath: Ind\
2 exPath) -> UITableViewCell {
3 // Dequeue Reusable Cell
4 guard let cell = tableView.dequeueReusableCell(withIdentifier: N\
5 oteTableViewCell.reuseIdentifier, for: indexPath) as? NoteTableViewC\
6 ell else {
7 fatalError("Unexpected Index Path")
8 }
9
10 // Configure Cell
11 configure(cell, at: indexPath)
12
13 return cell
14 }
We can now use this helper method to update the table view cell that
corresponds with the managed object that was updated. We safely unwrap the
value of indexPath, fetch the table view cell that corresponds with the index
path, and use the configure(_:at:) method to update the contents of the table
view cell.
NotesViewController.swift
1 case .update:
2 if let indexPath = indexPath, let cell = tableView.cellForRow(at\
3 : indexPath) as? NoteTableViewCell {
4 configure(cell, at: indexPath)
5 }
Moves
When a managed object is modified, it can impact the sort order of the
managed objects. This isn’t easy to implement from scratch. Fortunately, the
fetched results controller takes care of this as well through the move type.
NotesViewController.swift
1 case .move:
2 if let indexPath = indexPath {
3 tableView.deleteRows(at: [indexPath], with: .fade)
4 }
5
6 if let newIndexPath = newIndexPath {
7 tableView.insertRows(at: [newIndexPath], with: .fade)
8 }
Notice that we now also have animations. This is something we didn’t have
with the previous implementation because we reloaded the table view with
every change.
Before We Start
In this chapter, I show you how to assign a category to a note. I’ve already
created view controllers for creating, updating, and deleting categories. You
should already know how that works.
The scenes for these view controllers are located in the Categories
storyboard. We use a storyboard reference to navigate from the Notes
storyboard to the Categories storyboard. Storyboard references are a very
nice addition to UIKit and make storyboards more manageable and more
appealing for larger projects.
I also updated the user interface of the note view controller. It now also
displays the category to which the note belongs. If a note doesn’t have a
category, we show that it doesn’t belong to a category yet. The Edit button on
the right takes the user to the categories view controller.
The first step we need to take is passing the Note instance to the categories
view controller. Open CategoriesViewController.swift, remove the
managedObjectContext property, and declare a property, note, of type Note?.
CategoriesViewController.swift
1 import UIKit
2 import CoreData
3
4 class CategoriesViewController: UIViewController {
5
6 ...
7
8 // MARK: - Properties
9
10 @IBOutlet var messageLabel: UILabel!
11 @IBOutlet var tableView: UITableView!
12
13 // MARK: -
14
15 var note: Note?
16
17 ...
18
19 }
We no longer need a property for the managed object context because we can
access the managed object context through the note property. This means we
need to make three small changes in the CategoriesViewController class.
CategoriesViewController.swift
1 fileprivate lazy var fetchedResultsController: NSFetchedResultsContr\
2 oller<Category> = {
3 guard let managedObjectContext = self.note?.managedObjectContext\
4 else {
5 fatalError("No Managed Object Context Found")
6 }
7
8 ...
9 }()
CategoriesViewController.swift
1 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
2 guard let identifier = segue.identifier else { return }
3
4 switch identifier {
5 case Segue.AddCategory:
6 ...
7
8 // Configure Destination
9 destination.managedObjectContext = note?.managedObjectContext
10 case Segue.Category:
11 ...
12 default:
13 break
14 }
15 }
CategoriesViewController.swift
1 func tableView(_ tableView: UITableView, commit editingStyle: UITabl\
2 eViewCellEditingStyle, forRowAt indexPath: IndexPath) {
3 ...
4
5 // Delete Category
6 note?.managedObjectContext?.delete(category)
7 }
Before we move on, we need to set the note property of the categories view
controller in the prepare(for:sender:) method of the NoteViewController class.
Open NoteViewController.swift and navigate to the prepare(for:sender:)
method. Instead of setting the managedObjectContext property, which we
deleted a minute ago, we set the note property. That’s it.
NoteViewController.swift
1 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
2 guard let identifier = segue.identifier else { return }
3
4 switch identifier {
5 case Segue.Categories:
6 guard let destination = segue.destination as? CategoriesView\
7 Controller else {
8 return
9 }
10
11 // Configure Destination
12 destination.note = note
13 default:
14 break
15 }
16 }
CategoriesViewController.swift
1 func configure(_ cell: CategoryTableViewCell, at indexPath: IndexPat\
2 h) {
3 // Fetch Note
4 let category = fetchedResultsController.object(at: indexPath)
5
6 // Configure Cell
7 cell.nameLabel.text = category.name
8
9 if note?.category == category {
10 cell.nameLabel.textColor = .bitterSweet
11 } else {
12 cell.nameLabel.textColor = .black
13 }
14 }
What’s most interesting to us is what happens when the user taps a category
in the table view. We ask the fetched results controller for the category that
corresponds with the value of indexPath, we set the category property of the
note, and we pop the categories view controller from the navigation stack.
CategoriesViewController.swift
1 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: I\
2 ndexPath) {
3 tableView.deselectRow(at: indexPath, animated: true)
4
5 // Fetch Category
6 let category = fetchedResultsController.object(at: indexPath)
7
8 // Update Note
9 note?.category = category
10
11 // Pop View Controller From Navigation Stack
12 let _ = navigationController?.popViewController(animated: true)
13 }
This also means that it doesn’t matter which side of the relationship you
update. The result is identical. That’s important to understand and remember.
Assigning a Note to a Category
Even though the application doesn’t have the ability to assign a note to a
category, I’d like to show you how you can assign a note to a category.
Remember that a category can have many notes. We cannot simply set the
notes property of the category to add a note to a category. That won’t work.
The notes property of a category is of type NSSet?. You may be wondering how
to best add a note to that set. The set isn’t mutable. Fortunately, Xcode has
you covered.
Earlier in the book, I explained that Xcode generates some code for us. It
automatically generates an NSManagedObject subclass for every entity if you
check Class Definition in the Codegen section of the Data Model Inspector.
But Xcode does more than generate a class definition. It also generates
convenience methods for adding managed objects to a To-Many relationship.
The format of these convenience methods is easy to remember. For the notes
relationship, for example, it is:
Long story short, we can add a note to a category by passing the note as an
argument of the addToNotes(_:) method. It’s that simple.
And remember that we only need to set one side of the relationship. The other
side is automatically updated for us. To make it easy on ourselves, we set the
category property of the note. Run the application to try it out.
It seems to work, but the note view controller doesn’t update the value of the
category label. This isn’t surprising since we haven’t put any code in place that
updates the category label when the note of the note view controller is
modified.
NoteViewController.swift
1 override func viewDidLoad() {
2 super.viewDidLoad()
3
4 title = "Edit Note"
5
6 setupView()
7
8 setupNotificationHandling()
9 }
NoteViewController.swift
1 // MARK: - Helper Methods
2
3 private func setupNotificationHandling() {
4 let notificationCenter = NotificationCenter.default
5 notificationCenter.addObserver(self,
6 selector: #selector(managedObject\
7 ContextObjectsDidChange(_:)),
8 name: Notification.Name.NSManaged\
9 ObjectContextObjectsDidChange,
10 object: note?.managedObjectContex\
11 t)
12 }
In this method, we make sure that the userInfo dictionary of the notification
isn’t equal to nil and that it contains a value for the NSUpdatedObjectsKey key.
We then use a fancy line of code to make sure the note of the note view
controller is one of the managed objects that was updated. We filter the
updates set of managed objects and, if the resulting set contains any managed
objects, we invoke updateCategoryLabel(), another helper method.
NoteViewController.swift
1 // MARK: - Notification Handling
2
3 @objc private func managedObjectContextObjectsDidChange(_ notificati\
4 on: Notification) {
5 guard let userInfo = notification.userInfo else { return }
6 guard let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSMana\
7 gedObject> else { return }
8
9 if (updates.filter { return $0 == note }).count > 0 {
10 updateCategoryLabel()
11 }
12 }
NoteViewController.swift
1 private func updateCategoryLabel() {
2 // Configure Category Label
3 categoryLabel.text = note?.category?.name ?? "No Category"
4 }
Run the application again and modify the category of a note to make sure
everything is working as expected.
20 Adding a Dash of Color
In this chapter, I’d like to add the ability to assign a color to a category. This
makes it easier to visualize which category a note belongs to.
Before We Start
I’ve already laid the groundwork for this feature. Let me show you what we
start with.
The category view controller contains a view for displaying the color of its
category. Tapping the color view takes the user to the color view controller, the
view controller responsible for picking a color.
To pick a color, the user needs to adjust three sliders in the color view
controller. Because the color view controller is a component that we might
want to reuse, it doesn’t keep a reference to a category. We use a delegate
protocol to notify the category view controller which color the user has picked.
Color View Controller
This is a personal choice. I like this approach because it’s easier to read and it
results in minimal overhead in terms of converting the value to a UIColor
instance. Keep in mind that it isn’t possible to store UIColor instances in the
persistent store, we always need to convert it to another value, a String in this
example.
We could also convert it to binary data. That’s another common option. The
downside is that it’s harder to debug since most developers cannot read binary
data … or at least I can’t.
Before we move on, I want to make two changes to the attributes of the
colorAsHex attribute. With the colorAsHex attribute selected, open the Data
Model Inspector on the right and uncheck the Optional checkbox.
Requiring an Attribute
Remember that this makes the attribute required. It means a category cannot
be stored in the persistent store if it doesn’t have a value for the colorAsHex
attribute. This is fine because we’re going to make another change.
The Data Model Inspector also lets us define a default value for an attribute.
What does that mean? If we don’t explicitly assign a value to the colorAsHex
attribute, Core Data sets the value of the attribute to the value of the Default
Value field. Because we’re working with hex values, that’s very easy to do. In
the Default Value field, enter the hex value for white, FFFFFF. That’s another
benefit of working with hex values.
Extending UIColor
To make the conversion from and to hex values easier, I created an extension
for UIColor. You can find the extension in the Extensions group in
UIColor.swift.
UIColor.swift
1 import UIKit
2
3 extension UIColor {
4
5 static let bitterSweet = UIColor(red:0.99, green:0.47, blue:0.44\
6 , alpha:1.0)
7
8 }
9
10 extension UIColor {
11
12 // MARK: - Initialization
13
14 convenience init?(hex: String) {
15 var hexNormalized = hex.trimmingCharacters(in: .whitespacesA\
16 ndNewlines)
17 hexNormalized = hexNormalized.replacingOccurrences(of: "#", \
18 with: "")
19
20 // Helpers
21 var rgb: UInt32 = 0
22 var r: CGFloat = 0.0
23 var g: CGFloat = 0.0
24 var b: CGFloat = 0.0
25 var a: CGFloat = 1.0
26 let length = hexNormalized.characters.count
27
28 // Create Scanner
29 Scanner(string: hexNormalized).scanHexInt32(&rgb)
30
31 if length == 6 {
32 r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
33 g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
34 b = CGFloat(rgb & 0x0000FF) / 255.0
35
36 } else if length == 8 {
37 r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0
38 g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0
39 b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0
40 a = CGFloat(rgb & 0x000000FF) / 255.0
41
42 } else {
43 return nil
44 }
45
46 self.init(red: r, green: g, blue: b, alpha: a)
47 }
48
49 // MARK: - Convenience Methods
50
51 var toHex: String? {
52 // Extract Components
53 guard let components = cgColor.components, components.count \
54 >= 3 else {
55 return nil
56 }
57
58 // Helpers
59 let r = Float(components[0])
60 let g = Float(components[1])
61 let b = Float(components[2])
62 var a = Float(1.0)
63
64 if components.count >= 4 {
65 a = Float(components[3])
66 }
67
68 // Create Hex String
69 let hex = String(format: "%02lX%02lX%02lX%02lX", lroundf(r *\
70 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255))
71
72 return hex
73 }
74
75 }
Extending Category
We can go one step further and create an extension for the Category class to
abstract any interactions with hex values from the Category class. The goal is
that we only deal with UIColor instances.
Create a new Swift file in the Core Data > Extensions group and name it
Category. Add an import statement for UIKit and create an extension for the
Category class.
Category.swift
1 import UIKit
2
3 extension Category {
4
5 }
Category.swift
1 import UIKit
2
3 extension Category {
4
5 var color: UIColor? {
6 get {
7
8 }
9
10 set(newColor) {
11
12 }
13 }
14
15 }
Category.swift
1 get {
2 guard let hex = colorAsHex else { return nil }
3 return UIColor(hex: hex)
4 }
In the setter, we convert the UIColor instance to a hex value and update the
value of the colorAsHex property. This small improvement will keep the code
we write clean and focused.
Category.swift
1 set(newColor) {
2 if let newColor = newColor {
3 colorAsHex = newColor.toHex
4 }
5 }
CategoryViewController.swift
1 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
2 guard let identifier = segue.identifier else { return }
3
4 switch identifier {
5 case Segue.Color:
6 ...
7
8 // Configure Destination
9 destination.delegate = self
10 destination.color = category?.color ?? .white
11 default:
12 break
13 }
14 }
CategoryViewController.swift
1 func controller(_ controller: ColorViewController, didPick color: UI\
2 Color) {
3 // Update Category
4 category?.color = color
5
6 // Update View
7 updateColorView()
8 }
Last but not least, we need to update the updateColorView() method of the
category view controller. In this method, we set the background color of the
color view with the color of the category.
CategoryViewController.swift
1 private func updateColorView() {
2 // Configure Color View
3 colorView.backgroundColor = category?.color
4 }
I’m sure you agree that the color computed property keeps the implementation
focused by removing any logic related to value transformations.
NotesViewController.swift
1 func configure(_ cell: NoteTableViewCell, at indexPath: IndexPath) {
2 ...
3
4 if let color = note.category?.color {
5 cell.categoryColorView.backgroundColor = color
6 } else {
7 cell.categoryColorView.backgroundColor = .white
8 }
9 }
A Crash
Run the application to see the new feature in action. Wait. That doesn’t look
good. The application crashed. The persistent store coordinator wasn’t able to
add the persistent store.
If we take a closer look at the error message in the console, we see that the
persistent store isn’t compatible with the data model.
Console
1 CoreData: error: -addPersistentStoreWithType:SQLite configuration:(n\
2 ull) URL:file:///var/mobile/Containers/Data/Application/40950C64-3D8\
3 E-45AF-9890-CCAF59444996/Documents/Notes.sqlite options:(null) ... r\
4 eturned error Error Domain=NSCocoaErrorDomain Code=134100 "The manag\
5 ed object model version used to open the persistent store is incompa\
6 tible with the one that was used to create the persistent store." Us\
7 erInfo={metadata={
8 NSPersistenceFrameworkVersion = 832;
9 NSStoreModelVersionHashes = {
10 Category = <fa37182c b55c9960 577e91ae b9fc0c14 092dcec2 564\
11 459a1 19bb513f 45641c4a>;
12 Note = <b76dea89 b30116c0 c283030a e66c8678 e411956b fee2991\
13 0 fbbe70be 034a4d56>;
14 Tag = <b740a6fb 4c426dd1 4ea60b33 5a71b968 da756f6f e3482227\
15 2cb4a849 ebf7dc73>;
16 };
17 NSStoreModelVersionHashesVersion = 3;
18 NSStoreModelVersionIdentifiers = (
19 ""
20 );
21 NSStoreType = SQLite;
22 NSStoreUUID = "A36D0B58-5E20-4F2C-AC20-111EC9F0D0E3";
23 "_NSAutoVacuumLevel" = 2;
24 }, reason=The model used to open the store is incompatible with the \
25 one used to create the store} with userInfo dictionary {
26 metadata = {
27 NSPersistenceFrameworkVersion = 832;
28 NSStoreModelVersionHashes = {
29 Category = <fa37182c b55c9960 577e91ae b9fc0c14 092dcec2\
30 564459a1 19bb513f 45641c4a>;
31 Note = <b76dea89 b30116c0 c283030a e66c8678 e411956b fee\
32 29910 fbbe70be 034a4d56>;
33 Tag = <b740a6fb 4c426dd1 4ea60b33 5a71b968 da756f6f e348\
34 2227 2cb4a849 ebf7dc73>;
35 };
36 NSStoreModelVersionHashesVersion = 3;
37 NSStoreModelVersionIdentifiers = (
38 ""
39 );
40 NSStoreType = SQLite;
41 NSStoreUUID = "A36D0B58-5E20-4F2C-AC20-111EC9F0D0E3";
42 "_NSAutoVacuumLevel" = 2;
43 };
44 reason = "The model used to open the store is incompatible with \
45 the one used to create the store";
46 }
The managed object model version used to open the persistent store is
incompatible with the one that was used to create the persistent store.
Earlier in this book, we ran into the same issue and I told you we would tackle
this problem later in the book. Well … it’s time to talk about migrations.
21 Data Model Migrations
An application that grows and gains features also gains new requirements.
The data model, for example, grows and changes. Core Data handles changes
pretty well as long as you play by the rules of the framework.
In this chapter, we take a close look at the cause of the crash we ran into in
the previous chapter. We learn how Core Data helps us manage changes of
the data model and what pitfalls we absolutely need to avoid.
CoreDataManager.swift
1 private lazy var persistentStoreCoordinator: NSPersistentStoreCoordi\
2 nator = {
3 ...
4
5 do {
6 // Add Persistent Store
7 try persistentStoreCoordinator.addPersistentStore(ofType: NS\
8 SQLiteStoreType, configurationName: nil, at: persistentStoreURL, opt\
9 ions: nil)
10
11 } catch {
12 fatalError("Unable to Add Persistent Store")
13 }
14
15 return persistentStoreCoordinator
16 }()
In this chapter, I want to show you what we need to do to prevent that adding
the persistent store to the persistent store coordinator fails. Run the application
again and inspect the output in the console. This line tells us what went wrong.
1 reason = "The model used to open the store is incompatible with the \
2 one used to create the store";
Finding the Root Cause
We’re getting closer to the root of the problem. Core Data tells us that the data
model isn’t compatible with the data model we used to create the persistent
store. What does that mean?
In the previous chapter, we modified the data model by adding the colorAsHex
attribute to the Category entity. With the new data model in place, we ran the
application again … and you know what happened next.
The error message in the console indicates that the data model that was used
to create the persistent store isn’t identical to the current data model. As a
result, Core Data bails out and throws an error.
But how do you tell Core Data about the changes you made to the data
model? The answer is versioning.
The idea is simple. Core Data tells us that the current data model is not the
one that was used to create the persistent store. To solve that problem, we
first and foremost leave the data model that was used to create the persistent
store untouched. That’s one problem solved.
To make changes to the data model, we make a new version of the data
model. Each data model version has a unique identifier and Core Data stores
this identifier in the persistent store to know what model was used to create the
persistent store.
Before the persistent store coordinator adds a persistent store, it compares the
unique identifier stored in the persistent store with that of the current data
model.
Now that we know what went wrong, we can implement a solution. Fortunately,
Core Data makes versioning the data model very easy.
Before You Go
I already mentioned several times that you shouldn’t throw a fatal error if
adding the persistent store fails in production. It’s fine to throw a fatal error if
you’re developing your application. Once your application is in the hands of
users, though, you need to handle the situation more gracefully.
How you handle failing to add a persistent store depends on your application.
To recover from this scenario, you could delete the persistent store and try
adding the persistent store again. If that operation fails as well, then you have
bigger problems to worry about. Always remember that deleting the persistent
store is synonymous to data loss. Try to avoid this at all cost.
The first action I usually take is moving the existing persistent store (the one
that cannot be added to the persistent store coordinator) to a new location to
prevent data loss. This doesn’t resolve the issue, but it prevents immediate
data loss. You can then safely add a new persistent store to the persistent
store coordinator without losing the user’s data. This means the user can
continue using the application without running into a crash. It can also help
debugging the issue if you add a mechanism that enables the user to send you
the corrupt persistent store.
The second action is notifying the user about the problem. If the user opens
your application and sees it’s empty, they think they lost their data. It’s
important that you inform them about the situation and how to handle it.
Explain the problem in easy to understand words. Make sure they don’t panic
and ask them to get in touch with you to debug the issue.
I want to emphasize that there’s no one solution to this problem. The point I
want to drive home is that you need to prepare for this scenario. If this
happens, it doesn’t necessarily mean you made a mistake. But it does mean
that it’s up to you to solve the problem.
22 Versioning the Data Model
In the previous chapter, we exposed the root cause of the crash we ran into
earlier. The solution is versioning the data model.
Because we reverted the data model to its original state, the application should
no longer crash.
Don’t worry about any errors that pop up. Because we removed the colorAsHex
attribute, the compiler complains that the Category class doesn’t have a
property named colorAsHex. We’ll fix that in a minute.
Notice that a small triangle has appeared on the left of the data model in the
Project Navigator. You can click the triangle to show the list of data model
versions.
But that’s not what we have in mind. Before we make any changes, select
Notes.xcdatamodeld, not Notes.xcdatamodel. Open the File Inspector on
the right and set Model Version to Notes 2, the data model version we just
added.
Changing the Active Data Model Version
Because we haven’t run the application yet, we can still modify the new data
model version without running into compatibility issues. Select Notes
2.xcdatamodel and add the colorAsHex attribute to the Category entity.
Don’t forget to uncheck the Optional checkbox and set Default Value to
white.
Run the application to see if we solved the incompatibility problem we ran into
earlier. Are you still running into a crash? To make changes to the data model,
we’ve added a new data model version. We also marked the new data model
version as the active data model version.
Performing Migrations
I already told you that a persistent store is tied to a particular version of the
data model. It keeps a reference to the unique identifier of the data model. If
the data model changes, we need to tell Core Data how to migrate the data of
the persistent store to the new data model version.
- lightweight migrations
- heavyweight migrations
Heavyweight migrations are complex and you should try to avoid them
whenever possible. Heavyweight migrations migrations are an advanced topic
and they’re not covered in this book. Lightweight migrations are much easier
because Core Data takes care of the heavy lifting.
- NSMigratePersistentStoresAutomaticallyOption
- NSInferMappingModelAutomaticallyOption
CoreDataManager.swift
1 let options = [
2 NSMigratePersistentStoresAutomaticallyOption : true,
3 NSInferMappingModelAutomaticallyOption : true
4 ]
What is a mapping model? A mapping model defines how one version of the
data model relates to another version. For lightweight migrations, Core Data
can infer the mapping model by inspecting the data model versions. This isn’t
true for heavyweight migrations and that’s what makes heavyweight migrations
complex and tedious. For heavyweight migrations, the developer is
responsible for creating the mapping model.
With this in mind, we can update the implementation of the do clause of the do-
catch statement in the CoreDataManager class. This is what the updated
implementation looks like.
CoreDataManager.swift
1 private lazy var persistentStoreCoordinator: NSPersistentStoreCoordi\
2 nator = {
3 ...
4
5 do {
6 let options = [
7 NSMigratePersistentStoresAutomaticallyOption : true,
8 NSInferMappingModelAutomaticallyOption : true
9 ]
10
11 // Add Persistent Store
12 try persistentStoreCoordinator.addPersistentStore(ofType: NS\
13 SQLiteStoreType,
14 configurat\
15 ionName: nil,
16 at: persis\
17 tentStoreURL,
18 options: o\
19 ptions)
20
21 } catch {
22 fatalError("Unable to Add Persistent Store")
23 }
24
25 return persistentStoreCoordinator
26 }()
Run the application to see if the solution works. If you don’t run into a crash,
then Core Data successfully migrated the persistent store to the new data
model version.
Keep It Lightweight
Whenever you make a change to a data model, you need to consider the
consequences. Lightweight migrations carry little overhead. Heavyweight
migrations, however, are a pain. Really. Avoid them if possible.
It’s fine to make incremental changes to the data model as your application
grows, but once your application is in the hands of users you need to make
sure they don’t lose their data due to a problematic migration. And always test
migrations before shipping a new version of your application.
Migrations are an important aspect of Core Data because most applications
grow and need to make changes to the data model at some point. Data model
changes and migrations aren’t hard, but they require attention and testing.
23 Assigning Tags to a Note
The last feature I want to add is the ability to tag notes. This feature is
interesting because of the Many-To-Many relationship of the Note and Tag
entities. In this chapter, you learn how to work with such a relationship.
Before We Start
I already created view controllers for creating, updating, and deleting tags. This
is very similar to managing notes and categories. This should be familiar by
now.
The scenes for these view controllers are located in the Tags storyboard. We
use another storyboard reference to navigate from the Notes storyboard to the
Tags storyboard.
I also updated the user interface of the note view controller. In addition to the
category of the note, it displays the tags of the note. If a note doesn’t have any
tags, we show a message saying that it doesn’t have any tags yet. The Edit
button on the right takes the user to the tags view controller.
We also display the tags of a note in the notes view controller, below the title of
the note. To make this work, I added a label to the NoteTableViewCell class,
below the title label.
Note Table View Cell
Note.swift
1 import Foundation
2
3 extension Note {
4
5 // MARK: - Dates
6
7 ...
8
9 // MARK: - Tags
10
11 var alphabetizedTags: [Tag]? {
12
13 }
14
15 var alphabetizedTagsAsString: String? {
16
17 }
18
19 }
Note.swift
1 var alphabetizedTags: [Tag]? {
2
3 }
First, we make sure the tags property of the note isn’t equal to nil. We also
cast the value of the tags property to a set of Tag instances.
Note.swift
1 var alphabetizedTags: [Tag]? {
2 guard let tags = tags as? Set<Tag> else {
3 return nil
4 }
5 }
We then sort the set of tags. We need to jump through a few hoops to make
this work. Remember that, even though the name attribute of the Tag entity is
required, the name property of the Tag class is an optional. We discussed the
reason for this earlier in this book. The optionality of the name property makes
the closure of the sorted(by:) method a bit verbose. But, thanks to the syntax
of the Swift language, the result doesn’t look too bad.
Note.swift
1 var alphabetizedTags: [Tag]? {
2 guard let tags = tags as? Set<Tag> else {
3 return nil
4 }
5
6 return tags.sorted(by: {
7 guard let tag0 = $0.name else { return true }
8 guard let tag1 = $1.name else { return true }
9 return tag0 < tag1
10 })
11 }
Now that we have access to an alphabetized array of tags, we can turn that
array into a string. We implement another computed property,
alphabetizedTagsAsString, of type String?. The tags in the string should be
separated by commas.
Note.swift
1 var alphabetizedTagsAsString: String? {
2
3 }
If a note doesn’t have any tags, we return nil. We could return a placeholder
string, for example, “No Tags”, but I prefer to keep implementation details like
that out of the model layer. That’s the responsibility of the controller or, if
you’re using MVVM, the view model. If the computed property is equal to nil,
it’s up to the controller or the view model to decide how to respond.
Note.swift
1 var alphabetizedTagsAsString: String? {
2 guard let tags = alphabetizedTags, tags.count > 0 else {
3 return nil
4 }
5 }
In the next step, we convert the array of tags to an array of strings. We use
flatMap(_:) to extract the name of each tag from the array of tags. The
advantage of using flatMap(_:) is that it automatically skips any tags that don’t
have a name. This shouldn’t happen, but remember that the name property is of
type String?.
Note.swift
1 var alphabetizedTagsAsString: String? {
2 guard let tags = alphabetizedTags, tags.count > 0 else {
3 return nil
4 }
5
6 let names = tags.flatMap { $0.name }
7 }
We turn the array of strings into a string using the joined(separator:) method.
Note.swift
1 var alphabetizedTagsAsString: String? {
2 guard let tags = alphabetizedTags, tags.count > 0 else {
3 return nil
4 }
5
6 let names = tags.flatMap { $0.name }
7 return names.joined(separator: ", ")
8 }
NotesViewController.swift
1 func configure(_ cell: NoteTableViewCell, at indexPath: IndexPath) {
2 // Fetch Note
3 let note = fetchedResultsController.object(at: indexPath)
4
5 // Configure Cell
6 cell.titleLabel.text = note.title
7 cell.contentsLabel.text = note.contents
8 cell.tagsLabel.text = note.alphabetizedTagsAsString ?? "No Tags"
9 cell.updatedAtLabel.text = updatedAtDateFormatter.string(from: n\
10 ote.updatedAtAsDate)
11
12 ...
13 }
NoteViewController.swift
1 private func updateTagsLabel() {
2 // Configure Tags Label
3 tagsLabel.text = note?.alphabetizedTagsAsString ?? "No Tags"
4 }
TagsViewController.swift
1 func configure(_ cell: TagTableViewCell, at indexPath: IndexPath) {
2 // Fetch Tag
3 let tag = fetchedResultsController.object(at: indexPath)
4
5 // Configure Cell
6 cell.nameLabel.text = tag.name
7
8 if let containsTag = note?.tags?.contains(tag), containsTag == t\
9 rue {
10 cell.nameLabel.textColor = .bitterSweet
11 } else {
12 cell.nameLabel.textColor = .black
13 }
14 }
The user should be able to add and remove tags by tapping a tag in the table
view. To add this ability, we update the implementation of the
tableView(_:didSelectRowAt:) method of the UITableViewDelegate protocol.
We fetch the tag that corresponds with the value of the indexPath parameter
and ask the note if it contains that tag. If it does, we remove it by invoking
removeFromTags(_:), one of the convenience methods that’s automatically
generated for us by Xcode. If the note doesn’t contain the tag, we add it by
invoking addToTags(_:), passing in the tag.
TagsViewController.swift
1 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: I\
2 ndexPath) {
3 tableView.deselectRow(at: indexPath, animated: true)
4
5 // Fetch Tag
6 let tag = fetchedResultsController.object(at: indexPath)
7
8 if let containsTag = note?.tags?.contains(tag), containsTag == t\
9 rue {
10 note?.removeFromTags(tag)
11 } else {
12 note?.addToTags(tag)
13 }
14 }
Because the tags view controller is powered by a fetched results controller, the
table view is automatically updated for us. The same is true for the notes view
controller. The fetched results controller detects the addition or deletion of a
tag and automatically updates the table view.
NoteViewController.swift
1 @objc private func managedObjectContextObjectsDidChange(_ notificati\
2 on: Notification) {
3 guard let userInfo = notification.userInfo else { return }
4 guard let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSMana\
5 gedObject> else { return }
6
7 if (updates.filter { return $0 == note }).count > 0 {
8 updateTagsLabel()
9 updateCategoryLabel()
10 }
11 }
That’s it. That’s how easy it is to work with a Many-To-Many relationship using
the Core Data framework. Run the application and create a few tags. Assign a
tag to a note to see the result of the changes we made.
24 Working In a Multithreaded Environment
We currently use one managed object context, which we created in the
CoreDataManager class. In the application, we pass the managed object context
to the objects that need it. This works fine, but there will be times when one
managed object context won’t cut it.
What happens if you access the same managed object context from different
threads? What happens if you pass a managed object from a background
thread to the main thread? But let’s start with the basics.
Concurrency Basics
Before we explore solutions for using Core Data in a multithreaded
environment, we need to know how Core Data behaves on multiple threads.
The documentation is very clear about this. Core Data expects to be run on a
single thread. Even though that thread doesn’t have to be the main thread,
Core Data wasn’t designed to be used on different threads.
The Core Data team at Apple isn’t naive, though. It knows that a persistence
framework needs to be accessible from multiple threads. A single thread may
be fine for many applications, but more complex applications need a robust,
multithreaded persistence framework.
Managed Objects
Instances of the NSManagedObject class should never be passed from one
thread to another. That’s a simple rule you need to respect. If you need to pass
a managed object from one thread to another, you use a managed object’s
objectID property.
Know that object(with:) throws an exception if no record can be found for that
object identifier. For example, if the application deleted the record
corresponding with the object identifier, Core Data is unable to hand your
application the corresponding record. The result is an exception.
The object identifier of a record is similar, but not identical, to the primary key
of a database record. It uniquely identifies the record and enables your
application to fetch a particular record regardless of what thread the operation
is performed on.
If the application asks a managed object context for a managed object with a
particular object identifier, the managed object context first looks if a managed
object with that object identifier is registered in the managed object context. If
there isn’t, the managed object is fetched or returned as a fault. We discuss
faults later in this book. Don’t worry about it for now.
Fetching a Managed Object
It’s fine to use a single persistent store coordinator that’s accessed by multiple
managed object contexts from different threads. This makes Core Data
concurrency a little bit easier.
Managing Concurrency
Core Data has come a long way and it used to be a nightmare to use Core
Data in a multithreaded environment. You still need to be careful when using
Core Data on multiple threads, but it’s become easier since iOS 6. Apple
added a number of useful APIs to the Core Data framework to make your life
as a developer easier.
Theory
Before ending this chapter, I want to talk about parent and child managed
object contexts, a topic I briefly mentioned in the introduction of this book.
Complex applications that heavily rely on Core Data can run into problems if
changes of the managed object context are written to the persistent store on
the main thread. Even on modern devices, such operations can result in the
main thread being blocked. Because the main thread is also used to update
the user interface of your application, the user experiences this as the
application freezing up for a moment.
This can be avoided by slightly modifying the Core Data stack of the
application. The approach I mostly use looks something like this.
The managed object context linked to the persistent store coordinator isn’t
associated with the main thread. Instead, it performs its work on a private
queue, not on the main queue. When the private managed object context
saves its changes, the write operation is performed on that private queue in
the background.
The private managed object context has a child managed object context,
which serves as the main managed object context of the application. The
concept of parent-child managed object contexts is key in this scenario.
When a child managed object context saves its changes, it pushes them to the
parent managed object context. In other words, when a child managed object
context saves its changes, the persistent store coordinator is unaware of the
save operation. It’s only when the parent managed object context performs a
save operation that the changes are pushed to the persistent store coordinator
and subsequently to the persistent store.
A child managed object context pushes its changes to its
parent managed object context.
Because no write operations (no disk I/O) are performed when a child
managed object context saves its changes, pushing changes from a child
managed object context to its parent is fast and efficient. This also means that
the queue on which the operation is performed isn’t blocked by a write
operation. That’s why the main managed object context of the application is
the child managed object context of a managed object context that operates in
the background on a private queue.
Practice
It’s time to put this into practice by updating the CoreDataManager class. In the
next chapter, we refactor the CoreDataManagerclass to make it more suitable for
use in a multithreaded environment.
25 Updating the Core Data Manager for
Concurrency
It’s time to refactor the CoreDataManager class. Let me walk you through the
changes we need to make. Don’t worry, though, most of the implementation of
the CoreDataManager class remains unchanged.
CoreDataManager.swift
1 private lazy var privateManagedObjectContext: NSManagedObjectContext\
2 = {
3
4 }()
- mainQueueConcurrencyType
- confinementConcurrencyType
- privateQueueConcurrencyType
CoreDataManager.swift
1 // Initialize Managed Object Context
2 let managedObjectContext = NSManagedObjectContext(concurrencyType: .\
3 privateQueueConcurrencyType)
CoreDataManager.swift
1 // Configure Managed Object Context
2 managedObjectContext.persistentStoreCoordinator = self.persistentSto\
3 reCoordinator
We return the managed object context from the closure. This is what the
implementation of the privateManagedObjectContext property looks like.
CoreDataManager.swift
1 private lazy var privateManagedObjectContext: NSManagedObjectContext\
2 = {
3 // Initialize Managed Object Context
4 let managedObjectContext = NSManagedObjectContext(concurrencyTyp\
5 e: .privateQueueConcurrencyType)
6
7 // Configure Managed Object Context
8 managedObjectContext.persistentStoreCoordinator = self.persisten\
9 tStoreCoordinator
10
11 return managedObjectContext
12 }()
CoreDataManager.swift
1 private(set) lazy var mainManagedObjectContext: NSManagedObjectConte\
2 xt = {
3 ...
4 }()
With what we learned in the previous chapter still fresh in your mind, the
change we need to make is easy. The managed object context is created by
invoking init(concurrencyType:), passing in mainQueueConcurrencyType as its
argument. Instead of setting the persistentStoreCoordinator property of the
managed object context, we set its parent property to the private managed
object context we created a few moments ago.
CoreDataManager.swift
1 private(set) lazy var mainManagedObjectContext: NSManagedObjectConte\
2 xt = {
3 // Initialize Managed Object Context
4 let managedObjectContext = NSManagedObjectContext(concurrencyTyp\
5 e: .mainQueueConcurrencyType)
6
7 // Configure Managed Object Context
8 managedObjectContext.parent = self.privateManagedObjectContext
9
10 return managedObjectContext
11 }()
This means that a save operation pushes changes from the main managed
object context to the private managed object context. From a performance
point of view, this is more than sufficient for the vast majority of applications
that make use of Core Data.
Saving those changes needs to happen on the queue of the managed object
context. But how do we know what queue that is? And how can we access that
queue?
Fortunately, the Core Data framework can help us with that. To make sure a
managed object context is accessed on the queue it’s associated with, you use
the perform(_:) and performAndWait(_:) methods.
Both methods accept a closure and the Core Data framework guarantees that
the closure is invoked on the queue the managed object context is associated
with. The only difference between both methods is that performAndWait(_:) is
performed synchronously. As the name implies, it blocks the thread it’s called
on.
CoreDataManager.swift
1 private func saveChanges() {
2 mainManagedObjectContext.performAndWait({
3 do {
4 if self.mainManagedObjectContext.hasChanges {
5 try self.mainManagedObjectContext.save()
6 }
7 } catch {
8 let saveError = error as NSError
9 print("Unable to Save Changes of Main Managed Object Con\
10 text")
11 print("\(saveError), \(saveError.localizedDescription)")
12 }
13 })
14 }
Next, we invoke perform(_:) on the private managed object context and in the
closure we save the changes of the private managed object context if it has
any. This means the private managed object context pushes its changes to the
persistent store coordinator.
CoreDataManager.swift
1 private func saveChanges() {
2 mainManagedObjectContext.performAndWait({
3 do {
4 if self.mainManagedObjectContext.hasChanges {
5 try self.mainManagedObjectContext.save()
6 }
7 } catch {
8 let saveError = error as NSError
9 print("Unable to Save Changes of Main Managed Object Con\
10 text")
11 print("\(saveError), \(saveError.localizedDescription)")
12 }
13 })
14
15 self.privateManagedObjectContext.perform({
16 do {
17 if self.privateManagedObjectContext.hasChanges {
18 try self.privateManagedObjectContext.save()
19 }
20 } catch {
21 let saveError = error as NSError
22 print("Unable to Save Changes of Private Managed Object \
23 Context")
24 print("\(saveError), \(saveError.localizedDescription)")
25 }
26 })
27 }
Notice that we first save the changes of the main managed object context. This
is important because we need to make sure the private managed object
context includes the changes of its child managed object context.
Another Option
There is another option. Take a look at this implementation of the
saveChanges() method.
CoreDataManager.swift
1 private func saveChanges() {
2 mainManagedObjectContext.perform({
3 do {
4 if self.mainManagedObjectContext.hasChanges {
5 try self.mainManagedObjectContext.save()
6 }
7 } catch {
8 let saveError = error as NSError
9 print("Unable to Save Changes of Main Managed Object Con\
10 text")
11 print("\(saveError), \(saveError.localizedDescription)")
12 }
13
14 self.privateManagedObjectContext.perform({
15 do {
16 if self.privateManagedObjectContext.hasChanges {
17 try self.privateManagedObjectContext.save()
18 }
19 } catch {
20 let saveError = error as NSError
21 print("Unable to Save Changes of Private Managed Obj\
22 ect Context")
23 print("\(saveError), \(saveError.localizedDescriptio\
24 n)")
25 }
26 })
27 })
28 }
The result is similar. We push the changes of the main managed object
context to the private managed object context by invoking save() on the main
managed object context. But notice that we use perform() instead of
performAndWait(_:).
After pushing the changes of the main managed object context to its parent,
we save the changes of the private managed object context. We do this within
the closure we pass to the perform(_:) method of the main managed object
context. This means that the changes of the main managed object context are
saved before those of the private managed object context are pushed to the
persistent store coordinator.
When to Save
We currently save the changes of the managed object contexts when it’s about
to be terminated and when the application is pushed to the background. This is
fine, but you need to keep in mind that a crash of your application results in
data loss if you adopt this strategy. Any changes that aren’t pushed to the
persistent store coordinator when the application is suddenly terminated are
lost.
Your knowledge of the Core Data framework has grown quite a bit. Even
though concurrency may seem to be an advanced topic, it isn’t. If you work
with Core Data, then you need to know how Core Data behaves in a
multithreaded environment.
26 Using a Better Core Data Stack
Because we updated the CoreDataManager class in the previous chapter, we
need to make a few changes in the project.
NotesViewController.swift
1 private lazy var fetchedResultsController: NSFetchedResultsControlle\
2 r<Note> = {
3 ...
4
5 // Create Fetched Results Controller
6 let fetchedResultsController = NSFetchedResultsController(fetchR\
7 equest: fetchRequest,
8 manage\
9 dObjectContext: self.coreDataManager.mainManagedObjectContext,
10 sectio\
11 nNameKeyPath: nil,
12 cacheN\
13 ame: nil)
14
15 ...
16 }()
NotesViewController.swift
1 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
2 guard let identifier = segue.identifier else { return }
3
4 switch identifier {
5 case Segue.AddNote:
6 ...
7
8 // Configure Destination
9 destination.managedObjectContext = coreDataManager.mainManag\
10 edObjectContext
11 case Segue.Note:
12 ...
13 default:
14 break
15 }
16 }
We repeat this change in the tableView(_:commit:forRowAt:) method of the
UITableViewDataSource protocol.
NotesViewController.swift
1 func tableView(_ tableView: UITableView, commit editingStyle: UITabl\
2 eViewCellEditingStyle, forRowAt indexPath: IndexPath) {
3 ...
4
5 // Delete Note
6 coreDataManager.mainManagedObjectContext.delete(note)
7 }
That’s it. Even though we could have kept the name of the managed object
context unchanged, the name of the property now clearly reflects the nature
and purpose of the managed object context. It’s associated with the main
queue and it’s this managed object context we need to use for any operations
related to the user interface. Because remember that the user interface of an
application should always be updated on the main thread.
27 Replacing the Core Data Manager Class
The CoreDataManager class is in charge of the Core Data stack of the
application. It encapsulates the Core Data stack and only exposes the main
managed object context to the rest of the application.
In the previous chapters, we improved the Core Data stack by using a private
parent managed object context and a child managed object context that
operates on the main queue of the application.
We also use dependency injection to pass the managed object context to the
objects that need it. That’s a good start. We could make a few other
improvements, but that’s not the focus of this chapter. This chapter focuses on
a brand new addition to the Core Data framework.
Persistent Container
For years, developers have created classes similar to the CoreDataManager
class because the framework itself didn’t provide a similar solution. That,
however, has changed very recently. During WWDC 2016, Apple introduced
the NSPersistentContainer class. This brand new member of the Core Data
framework is available as of iOS 10 and macOS 10.12. It looks and behaves
very similar to the CoreDataManager class we created.
The only difference with the CoreDataManager class is the name of the property.
The main managed object context is accessible through the viewContext
property. That’s another change we need to make.
NotesViewController.swift
1 private lazy var fetchedResultsController: NSFetchedResultsControlle\
2 r<Note> = {
3 ...
4
5 // Create Fetched Results Controller
6 let fetchedResultsController = NSFetchedResultsController(fetchR\
7 equest: fetchRequest,
8 manage\
9 dObjectContext: self.persistentContainer.viewContext,
10 sectio\
11 nNameKeyPath: nil,
12 cacheN\
13 ame: nil)
14
15 ...
16 }()
NotesViewController.swift
1 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
2 guard let identifier = segue.identifier else { return }
3
4 switch identifier {
5 case Segue.AddNote:
6 ...
7
8 // Configure Destination
9 destination.managedObjectContext = persistentContainer.viewC\
10 ontext
11 case Segue.Note:
12 ...
13 default:
14 break
15 }
16 }
NotesViewController.swift
1 func tableView(_ tableView: UITableView, commit editingStyle: UITabl\
2 eViewCellEditingStyle, forRowAt indexPath: IndexPath) {
3 ...
4
5 // Delete Note
6 persistentContainer.viewContext.delete(note)
7 }
You can ask a persistent container for a private managed object context by
invoking the newBackgroundContext() method. But we don’t need that in this
project.
If this is done on the main thread, it can block the main thread. And if this is
done during the launch of the application, the operating system can even
decide the application takes too long to launch and terminate the application.
That’s why some applications crash when you open them after an update. The
results can be dramatic. For example, the user could lose their data because
of a bad migration or a corrupted persistent store.
It’s also important that the application only accesses the Core Data stack after
it has successfully added the persistent store to the persistent store
coordinator.
The solution to these problems is simple. We need to add the persistent store
asynchronously on a background queue. And that’s an option the
NSPersistentContainer class offers us. But we need to make a few changes to
make this work in our project.
NotesViewController.swift
1 import UIKit
2 import CoreData
3
4 class NotesViewController: UIViewController {
5
6 ...
7
8 // MARK: - Properties
9
10 @IBOutlet var notesView: UIView!
11 @IBOutlet var messageLabel: UILabel!
12 @IBOutlet var tableView: UITableView!
13 @IBOutlet var activityIndicatorView: UIActivityIndicatorView!
14
15 ...
16
17 }
Open Notes.storyboard and add an activity indicator view to the notes view
controller scene. Make sure you add it to the view of the view controller, not
the notes view. Add the necessary constraints, open the Attributes Inspector
on the right, and check Animating and Hides When Stopped. Don’t forget to
connect the activity indicator view with the outlet we created a few moments
ago.
Adding an Activity Indicator View
NotesViewController.swift
1 override func viewDidLoad() {
2 super.viewDidLoad()
3
4 title = "Notes"
5
6 persistentContainer.loadPersistentStores { (persistentStoreDescr\
7 iption, error) in
8
9 }
10
11 setupView()
12 fetchNotes()
13 updateView()
14 }
But how does the persistent container know which persistent stores to add? By
setting the persistentStoreDescriptions property of the persistent container
you can specify which persistent stores to add. This property is of type
[NSPersistentStoreDescription], an array of NSPersistentStoreDescription
objects.
For a persistent container with one persistent store, though, the setup is
simple. If adding the persistent store is successful, the user interface is shown
to the user by invoking setupView(). We also invoke fetchNotes() to fetch the
user’s notes, and updateView(), to update the user interface. Notice that we
don’t explicitly dispatch the calls to setupView(), fetchNotes(), and
updateView() to the main thread. The documentation states that the completion
handler of loadPersistentStores(completionHandler:) is invoked on the calling
thread, the main thread in this example.
NotesViewController.swift
1 override func viewDidLoad() {
2 super.viewDidLoad()
3
4 title = "Notes"
5
6 persistentContainer.loadPersistentStores { (persistentStoreDescr\
7 iption, error) in
8 if let error = error {
9 print("Unable to Add Persistent Store")
10 print("\(error), \(error.localizedDescription)")
11
12 } else {
13 self.setupView()
14 self.fetchNotes()
15 self.updateView()
16 }
17 }
18 }
NotesViewController.swift
1 private func setupView() {
2 activityIndicatorView.stopAnimating()
3 notesView.isHidden = false
4
5 setupMessageLabel()
6 setupTableView()
7 }
Run the application to see the result. Adding the persistent store usually takes
very, very little time. This means that you won’t see the activity indicator view
when you launch the application because it’s already hidden when the
application is ready to use.
But there is one thing missing. The notes we created earlier. This is easy to
explain. The default location of the persistent store when using the
NSPersistentContainer class isn’t the same as the location we used for the
persistent store in the CoreDataManager class.
The old persistent store lives in the Documents directory whereas the new
persistent store is located in the Application Support directory of the Library
directory.
This is fine and we only run into this issue because we replaced the Core Data
manager with a NSPersistentContainer instance. If you want to modify the
default location of the persistent store, you need to subclass the
NSPersistentContainer class and override the defaultDirectoryURL() class
method.
Conclusion
It’s clear the NSPersistentContainer class is a welcome addition to the Core
Data framework. It fulfills the needs of many Core Data applications and it
offers a modern, easy-to-use API.
Make sure the application contains a few notes, a few categories, and a few
tags. That’s important to illustrate the concept of faulting.
Exploring Faults
Open NotesViewController.swift and navigate to the fetchNotes() method.
Replace the current implementation and create a fetch request for the Note
entity in a perform(_:) closure of the main managed object context.
NotesViewController.swift
1 private func fetchNotes() {
2 coreDataManager.mainManagedObjectContext.perform {
3 do {
4 // Create Fetch Request
5 let fetchRequest: NSFetchRequest<Note> = Note.fetchReque\
6 st()
7
8 // Fetch Notes
9 let notes = try fetchRequest.execute()
10
11 if let note = notes.first {
12 print(note)
13 }
14
15 } catch {
16 print(error)
17 }
18 }
19 }
We execute the fetch request and print the first note to the console. Run the
application to see the result. This is what the output looks like.
1 <Note: 0x1c009f4f0> (entity: Note; id: 0xd000000000040000 <x-coredat\
2 a://A36D0B58-5E20-4F2C-AC20-111EC9F0D0E3/Note/p1> ; data: <fault>)
You may not see anything unusual. We fetched a record of the Note entity. But
notice that the data associated with the record isn’t present. Instead we see
the word fault.
Now that you’ve seen faulting in action, it’s time to explain what it is and why
it’s so important for Core Data.
What Is a Fault
Core Data is a framework that’s incredibly performant thanks to the hard work
of the Core Data team at Apple. As you know, Core Data can only operate on
records of the persistent store once they’re loaded into memory.
This is only possible because Core Data is heavily optimized to keep its
memory footprint as low as possible. One of the techniques Core Data uses to
accomplish this is faulting.
Faulting wasn’t invented by the Core Data team at Apple. Several other
frameworks use a similar strategy to accomplish similar goals. Ruby on Rails
and Ember come to mind.
Even though faulting may look mysterious at first, the idea is simple. Core Data
only fetches the data it absolutely needs to satisfy the needs of your
application. In the above example, Core Data hasn’t fetched the property
values of the note yet. Why is that? Because we haven’t asked for it. Since we
don’t access any of the properties of the note, Core Data hasn’t bothered
fetching the note’s property values. Core Data is efficient and performant by
being lazy.
Firing a Fault
Let me show you how faulting works with another example. Below the print
statement, we safely unwrap the value of the title property and print it to the
console and we also add another print statement for the note.
NotesViewController.swift
1 private func fetchNotes() {
2 coreDataManager.mainManagedObjectContext.perform {
3 do {
4 // Create Fetch Request
5 let fetchRequest: NSFetchRequest<Note> = Note.fetchReque\
6 st()
7
8 // Fetch Notes
9 let notes = try fetchRequest.execute()
10
11 if let note = notes.first {
12 print(note)
13
14 if let title = note.title {
15 print(title)
16 }
17
18 print(note)
19 }
20
21 } catch {
22 print(error)
23 }
24 }
25 }
What’s happening here. We print the note to the console, ask the value of one
of the properties of the note, and print the note again. Why we do this
becomes clear when we inspect the results in the console. Run the application.
This is what the output looks like. Let’s break it down.
1 <Note: 0x1c409b620> (entity: Note; id: 0xd000000000040000 <x-coredat\
2 a://A36D0B58-5E20-4F2C-AC20-111EC9F0D0E3/Note/p1> ; data: <fault>)
3 My First Note
4 <Note: 0x1c409b620> (entity: Note; id: 0xd000000000040000 <x-coredat\
5 a://A36D0B58-5E20-4F2C-AC20-111EC9F0D0E3/Note/p1> ; data: {
6 category = "0xd000000000080002 <x-coredata://A36D0B58-5E20-4F2C-\
7 AC20-111EC9F0D0E3/Category/p2>";
8 contents = "Some text ...";
9 createdAt = "2017-07-06 07:22:18 +0000";
10 tags = "<relationship fault: 0x1c403e900 'tags'>";
11 title = "My First Note";
12 updatedAt = "2017-07-07 09:15:08 +0000";
13 })
The first print statement shows the fault we discussed earlier. This isn’t new.
1 <Note: 0x1c409b620> (entity: Note; id: 0xd000000000040000 <x-coredat\
2 a://A36D0B58-5E20-4F2C-AC20-111EC9F0D0E3/Note/p1> ; data: <fault>)
Despite this fault, we can access the value of the title property and print it to
the console. That’s interesting.
1 My First Note
And this is confirmed by the third print statement in which we print the note
again.
1 <Note: 0x1c409b620> (entity: Note; id: 0xd000000000040000 <x-coredat\
2 a://A36D0B58-5E20-4F2C-AC20-111EC9F0D0E3/Note/p1> ; data: {
3 category = "0xd000000000080002 <x-coredata://A36D0B58-5E20-4F2C-\
4 AC20-111EC9F0D0E3/Category/p2>";
5 contents = "Some text ...";
6 createdAt = "2017-07-06 07:22:18 +0000";
7 tags = "<relationship fault: 0x1c403e900 'tags'>";
8 title = "My First Note";
9 updatedAt = "2017-07-07 09:15:08 +0000";
10 })
Let me explain what’s happening under the hood. Core Data gives us what we
ask for and exactly that. Nothing more. We first asked the framework for the
user’s notes and Core Data diligently gave us the list of notes. But, as you can
see in the console, it’s a list of empty records.
From the moment we ask for the value of a property of one of the records,
Core Data jumps into action and fetches the data from the persistent store.
This is better known as firing a fault. But it doesn’t just fetch the value of the
title property. As you can see in the console, Core Data fetches the values of
every property of the note with the exception of relationships.
1 <Note: 0x1c409b620> (entity: Note; id: 0xd000000000040000 <x-coredat\
2 a://A36D0B58-5E20-4F2C-AC20-111EC9F0D0E3/Note/p1> ; data: {
3 category = "0xd000000000080002 <x-coredata://A36D0B58-5E20-4F2C-\
4 AC20-111EC9F0D0E3/Category/p2>";
5 contents = "Some text ...";
6 createdAt = "2017-07-06 07:22:18 +0000";
7 tags = "<relationship fault: 0x1c403e900 'tags'>";
8 title = "My First Note";
9 updatedAt = "2017-07-07 09:15:08 +0000";
10 })
Notice that the value of the tags property is missing. Instead, Xcode displays
relationship fault. This means that the tags of the note haven’t been fetched
yet.
1 tags = "<relationship fault: 0x1c403e900 'tags'>";
And the same applies to the category property. Even though it seems as if
Core Data has fetched the data for the category record of the note, it hasn’t.
1 category = "0xd000000000080002 <x-coredata://A36D0B58-5E20-4F2C-AC20\
2 -111EC9F0D0E3/Category/p2>";
This becomes clear if we print the value of the category property and run the
application again.
NotesViewController.swift
1 private func fetchNotes() {
2 coreDataManager.mainManagedObjectContext.perform {
3 do {
4 // Create Fetch Request
5 let fetchRequest: NSFetchRequest<Note> = Note.fetchReque\
6 st()
7
8 // Fetch Notes
9 let notes = try fetchRequest.execute()
10
11 if let note = notes.first {
12 print(note)
13
14 if let title = note.title {
15 print(title)
16 }
17
18 print(note)
19
20 if let category = note.category {
21 print(category)
22 }
23 }
24
25 } catch {
26 print(error)
27 }
28 }
29 }
I hope it’s starting to become clear that Core Data is very lazy … but in a good
way. It fetches the minimum amount of data to satisfy the needs of the
application.
NotesViewController.swift
1 private func fetchNotes() {
2 coreDataManager.mainManagedObjectContext.perform {
3 do {
4 // Create Fetch Request
5 let fetchRequest: NSFetchRequest<Note> = Note.fetchReque\
6 st()
7
8 // Fetch Notes
9 let notes = try fetchRequest.execute()
10
11 if let note = notes.first {
12 if let tags = note.tags as? Set<Tag> {
13 print(tags)
14
15 for tag in tags {
16 print(tag.name ?? "")
17 }
18 }
19 }
20
21 } catch {
22 print(error)
23 }
24 }
25 }
Core Data hands us a set of objects, but it hasn’t actually fetched the tag
records itself. The data of the tag records are faults.
1 [<Tag: 0x1c009a5e0> (entity: Tag; id: 0xd000000000040004 <x-coredata\
2 ://A36D0B58-5E20-4F2C-AC20-111EC9F0D0E3/Tag/p1> ; data: <fault>), <T\
3 ag: 0x1c009a540> (entity: Tag; id: 0xd000000000080004 <x-coredata://\
4 A36D0B58-5E20-4F2C-AC20-111EC9F0D0E3/Tag/p2> ; data: <fault>)]
The data is fetched the moment we access it. In this example, we ask each tag
for the value of its name property.
1 Monday
2 Family
When Core Data tries to fetch data from the persistent store that no longer
exists, it tells you it’s unable to fulfill the fault. In earlier versions of the
framework, Core Data would throw an exception, resulting in a crash of the
application.
Fortunately, Core Data has evolved over the years and the framework has
become better at handling issues like this. As of iOS 9 and macOS 10.11, the
NSManagedObjectContext class defines a new property,
shouldDeleteInaccessibleFaults. This property is set to true by default. But let
me show you what happens if this property is set to false. This is the old
behavior of the framework.
CoreDataManager.swift
1 private(set) lazy var mainManagedObjectContext: NSManagedObjectConte\
2 xt = {
3 // Initialize Managed Object Context
4 let managedObjectContext = NSManagedObjectContext(concurrencyTyp\
5 e: .mainQueueConcurrencyType)
6
7 // Configure Managed Object Context
8 managedObjectContext.shouldDeleteInaccessibleFaults = false
9 managedObjectContext.parent = self.privateManagedObjectContext
10
11 return managedObjectContext
12 }()
13
14 private lazy var privateManagedObjectContext: NSManagedObjectContext\
15 = {
16 // Initialize Managed Object Context
17 let managedObjectContext = NSManagedObjectContext(concurrencyTyp\
18 e: .privateQueueConcurrencyType)
19
20 // Configure Managed Object Context
21 managedObjectContext.shouldDeleteInaccessibleFaults = false
22 managedObjectContext.persistentStoreCoordinator = self.persisten\
23 tStoreCoordinator
24
25 return managedObjectContext
26 }()
Run the application and assign a category to a note. Push the application to
the background to save the changes and stop the application.
To show you the problem, I’m going to modify the persistent store. I only do
this to show you what could happen in production. We delete the category
record from the database.
To show the color of the category in the notes view controller, Core Data
needs to fetch the category for the note. But the category no longer exists
since we removed it from the persistent store, the SQLite database.
Even though we tampered with the database, this problem can also occur in
production. It used to drive developers unfamiliar with the framework crazy. As
of iOS 9 and macOS 10.11, Core Data gives developers the option to fail
elegantly by deleting any faults that are inaccessible.
29 Where to Go From Here
My hope is that this book has taught you that Core Data isn’t as difficult as
many developers believe it to be. Core Data isn’t complex once you
understand how the various pieces of the framework fit together.
I truly love working with Core Data because I like how the framework is
engineered and I know that it’s earned its stripes over the more than ten years
it’s been around. The automatic code generation introduced in Xcode 8 and
the nice additions the Swift language brings make the framework even better.
That said, keep in mind how the users of your application plan to use your
application. Take Notes as an example. We’ve used the application with a
handful of notes, but some users may have hundreds or thousands of notes.
How will that impact performance? Core Data is a robust persistence
framework and it’s a great fit for many applications that require a persistence
solution.
Testing
Remember to test the migrations of your application. This is an often
overlooked problem. Let me illustrate this with an example. Imagine a user
restores a backup of their device and installs an older version of your
application. After restoring the backup, they update your application to the
latest version, a version with a data model that differs from the old restored
version. This means the application needs to perform several migrations. And
if the user has many records stored in the persistent store, this can take
several seconds if not longer.
Even if this is an edge case, you need to understand that a problem may result
in data loss. Depending on the type of application, this can be a nightmare for
the user. You need to prevent data loss at all cost. The user trusts you with
their data. Don’t take that responsibility lightly.
Libraries
I also want to say a few words about third party libraries for Core Data. My
advise is to avoid them if possible. Core Data is a first party framework with a
great API. Instead of using a third party library, why don’t you write a small
library yourself with a handful of convenience methods. That goes a long way.
If you know and understand Core Data, then a wrapper around the framework
is unnecessary. Give it a try before choosing for more complexity and one
more dependency. Don’t you agree that the code we wrote in this book isn’t
complex? And this has nothing to do with the application. Core Data doesn’t
need to be complicated.
Data Model
In this book, I emphasized how important it is to take the time to create the
data model of your application. The data model can and will change over time,
but you only get a first try once.
Once the application is in the hands of your users, it’s a nightmare to make
drastic changes without running into problems. This isn’t a Core Data problem.
Most persistence solutions face these issues. That’s inherent to data
persistence.
Take your time to plan ahead. Keep it as simple as possible and only add
complexity if necessary.
Continue Learning
The Core Data framework has a lot more to offer than what you learned in this
book. It’s a very powerful and flexible framework. It’s true that most of the more
advanced features are used less frequently, but they can sometimes save you
a lot of time and frustration.
We covered a lot of ground in this book. It’s now time to use what you’ve
learned in your projects. If you have any feedback or questions, reach out to
me, send me an email ([email protected]), or talk to me on Twitter
(@_bartjacobs). I’m curious to hear your feedback and questions.
Table of Contents
Welcome 7
Xcode 9 and Swift 4 7
What You’ll Learn 7
How to Use This Book 8
1 What Is Core Data 9
Core Data Manages an Object Graph 9
When to Use Core Data 10
Core Data & SQLite 10
Core Data Goes Much Further 11
Drawbacks 11
2 Building Notes 12
3 Exploring the Core Data Stack 14
Managed Object Model 14
Managed Object Context 15
Persistent Store Coordinator 16
How Does Core Data Work 17
4 Creating the Project 20
5 Setting Up the Core Data Stack 22
Managed Object Context 25
Managed Object Model 26
Persistent Store Coordinator 26
Adding a Data Model 29
Setting Up the Core Data Stack 30
6 Core Data and Dependency Injection 32
7 Data Model, Entities, and Attributes 36
Compiling the Data Model 36
Exploring the Data Model Editor 37
What Is an Entity 38
Creating an Entity 38
Creating an Attribute 39
8 Data Model, Entities, and Relationships 42
Adding More Entities 42
Defining Relationships 43
Creating a To-One Relationship 43
Creating an Inverse Relationship 44
Creating a To-Many Relationship 46
Creating a Many-To-Many Relationship 47
9 Configuring Relationships 50
Delete Rules 50
Evaluating the Data Model 52
Another Scenario 53
More Configuration 53
10 Working With Managed Objects 55
What Is a Managed Object 55
Creating a Managed Object 55
Working With a Managed Object 60
Saving the Managed Object Context 60
11 Subclassing NSManagedObject 63
Code Generation 63
Convenience Methods 64
What to Remember 65
12 Adding a Note 66
Target Configuration 66
View Controllers 66
Adding Notes 68
13 Don’t Forget to Save 72
Revisiting the Core Data Manager 72
Saving Changes 73
Build and Run 74
14 Fetch Those Notes 75
Before We Start 75
Fetching Notes 76
Displaying Notes 80
15 Fix That Mistake 83
Before We Start 83
Passing a Note 83
Populating the Note View Controller 84
Updating a Note 85
Updating the Table View 86
Listening for Notifications 86
16 To the Trash Can 92
Deleting a Note 92
Build and Run 93
17 Introducing the Fetched Results Controller 94
Creating a Fetched Results Controller 94
Performing a Fetch Request 96
Updating the Table View 97
A Few More Changes 99
18 Exploring the NSFetchedResultsControllerDelegate Protocol 101
Implementing the Protocol 101
Inserts 103
Deletes 103
Updates 104
Moves 105
19 Adding Categories to the Mix 106
Before We Start 106
Assigning a Category to a Note 108
Assigning a Note to a Category 112
Updating the Note View Controller 113
20 Adding a Dash of Color 115
Before We Start 115
Updating the Data Model 116
Extending UIColor 117
Extending Category 119
Updating the Category View Controller 120
Updating the Notes View Controller 121
A Crash 122
21 Data Model Migrations 124
Finding the Root Cause 124
Versioning the Data Model 126
Before You Go 127
22 Versioning the Data Model 129
Restoring the Data Model 129
Adding a Data Model Version 129
Performing Migrations 132
Keep It Lightweight 134
Plan, Plan, Plan 134
23 Assigning Tags to a Note 136
Before We Start 136
Preparing the Note Class 139
Updating the Notes View Controller 141
Updating the Note View Controller 142
Updating the Tags View Controller 142
Tweaking the Note View Controller 143
24 Working In a Multithreaded Environment 145
Concurrency Basics 145
Managing Concurrency 147
Updating the Core Data Stack 147
Practice 150
25 Updating the Core Data Manager for Concurrency 151
Creating a Private Managed Object Context 151
Updating the Main Managed Object Context 152
Updating the Save Method 153
Another Option 155
When to Save 156
26 Using a Better Core Data Stack 157
Updating the Notes View Controller 157
27 Replacing the Core Data Manager Class 159
Persistent Container 159
Replacing the Core Data Manager 159
Adding the Persistent Store 161
Conclusion 166
28 Understanding Faulting 167
Exploring Faults 167
What Is a Fault 168
Firing a Fault 168
Faulting and Relationships 171
Unable to Fulfill Fault 172
29 Where to Go From Here 175
Core Data Isn’t Scary 175
Start Using Core Data 175
Testing 175
Libraries 176
Data Model 176
Continue Learning 176