In Core Data Beyond the Basics, we work with an application that manages clients and their invoices. I named the application Invoices. What's in a name? The application is similar to, for example, the mobile application of FreshBooks.
The user can see a list of their clients and invoices, and she can add, update, and remove clients and invoices. The AddClientViewController
class is responsible for adding a new client. There are several strategies for inserting a new record into the persistent store of the application. The solution I present in this tutorial is elegant and very often missed or overlooked by developers.
Data Model
The data model of Invoices isn't complicated. It contains two entities, Client and Invoice. The Client entity defines seven attributes:
- city of type String
- country of type String
- email of type String
- name of type String
- number of type String
- street of type String
- zip of type String
It also defines a to-many relationship with the Invoice entity, invoices.
The Invoice entity defines four attributes:
- amount of type Double
- currency of type String
- identifier of type String
- notes of type String
It defines a to-one relationship with the Client entity, client. It's the inverse relationships of the invoices relationship of the Client entity.
Managing State
The AddClientViewController
class shows the user a simple form she needs to fill out. The form is a plain table view with each of its table view cells holding a label and a text field.
Because the form is a table view, we need to temporarily hold onto the user's input to make sure it isn't lost when table view cells are reused by the table view.
How should the view controller handle the user's input? The add client view controller should only create a Client
record if the user taps the Save button and it should discard the contents of the form if the user taps the Back button. There are several options. Let's start with the most common and the most obvious option.
Properties to Save State
The view controller can define a property for each form element. Each of these properties temporarily stores the user's input.
class AddClientViewController: UITableViewController {
// MARK: - Properties
var city: String?
var country: String?
var email: String?
var name: String?
var number: String?
var street: String?
var zip: String?
...
}
When the user taps the Save button in the top right, the user's input is used to create and populate a Client
instance.
// MARK: - Actions
@IBAction func saveClient(_ sender: Any) {
// Initialize Client
let client = Client(context: managedObjectContext)
// Populate Cient
client.city = city
client.country = country
client.email = email
client.name = name
client.number = number
client.street = street
client.zip = zip
...
}
This approach works and it isn't a bad solution. But it isn't elegant. The view controller is responsible for managing the state of the form, which is something I'd like to avoid. Wouldn't it be better to create a Client
instance when the form is presented to the user and update the Client
instance as the user fills out the form? That brings us to the second option.
A Client Record to Save State
We could pass the main managed object context of the application to the add client view controller and lazily instantiate a Client
instance. The Client
instance is populated as the user fills out the form. This means that we insert a Client
instance into the main managed object context of the application when the user brings up the form.
class AddClientViewController: UITableViewController {
// MARK: - Properties
var managedObjectContext: NSManagedObjectContext!
// MARK: -
private lazy var client: Client = {
return Client(context: self.managedObjectContext)
}()
...
}
What happens if the user taps the back button? The Client
instance we created is still present in the managed object context. We could delete it if the add client view controller is dismissed and the Client
instance hasn't been saved.
Hmm ... this is starting to become smelly. It implies that we need to immediately save the managed object context when the user taps the Save button. This is fine, but it isn't something I usually do in a Core Data application. As I explain in Core Data Fundamentals, I prefer to push changes to the persistent store when the application is pushed to the background or when it's about to be terminated.
Let me introduce you to another option that is often discarded or overlooked. It's elegant and takes advantage of nested managed object contexts.
Using a Child Managed Object Context
For this option, you need to be familiar with nested managed object contexts. Developers are often reluctant to use parent and child managed object contexts because it seems overly complex and difficult. It really isn't as I illustrate in Core Data Fundamentals.
Let me show you how this works and why this option is an elegant solution to the problem we're trying to solve. We pass the main managed object context to the add client view controller. That doesn't change.
class AddClientViewController: UITableViewController {
// MARK: - Properties
var managedObjectContext: NSManagedObjectContext!
...
}
We also instantiate a Client
instance, which is populated as the user fills out the form. That doesn't change either. The difference is that we create a child managed object context and make the main managed object context the parent of the child managed object context. The Client
instance is inserted into the child managed object context, not into the main managed object context.
// MARK: -
private lazy var childManagedObjectContext: NSManagedObjectContext = {
// Initialize Managed Object Context
let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
// Configure Managed Object Context
managedObjectContext.parent = self.managedObjectContext
return managedObjectContext
}()
// MARK: -
private lazy var client: Client = {
return Client(context: self.childManagedObjectContext)
}()
Remember that a managed object is always associated with a managed object context. No exceptions. In this example, we insert the Client
instance into the child managed object context.
If you're unfamiliar with nested managed object contexts, then you're probably wondering what benefits this approach brings to the table. The benefits are exactly what we're looking for.
Push Changes on Save
A managed object context is usually associated with a persistent store coordinator. When the managed object context saves its changes, it pushes them to the persistent store coordinator it is tied to. The persistent store coordinator pushes the changes to the persistent store it manages. That's what most developers using Core Data are familiar with.
A child managed object context, however, isn't linked to a persistent store coordinator. Instead, it's tied to a parent managed object context. When the child managed object context saves its changes, it pushes them to the parent managed object context it's linked with. The changes are not pushed to the persistent store coordinator until the parent managed object context performs a save operation, pushing its changes to its persistent store coordinator.
I hope you're starting to see the bigger picture. By creating a child managed object context in the add client view controller and inserting the Client
instance into the child managed object context, no managed object is inserted into the main managed object context. If the user dismisses the view controller without saving the Client
instance, the child managed object context of the view controller is deallocated. Any unsaved changes are discarded, including the Client
instance we created in the add client view controller.
If the user decides to save the Client
instance by tapping the Save button, the changes of the child managed object context are pushed to the parent managed object context. Even though the child managed object context is deallocated when the view controller is dismissed, this isn't a problem since the Client
instance was pushed to the main managed object context, the parent managed object context of the child managed object context, when the user tapped the Save button.
Managed Object Contexts Are Cheap
A managed object context is cheap to create. Don't worry about performance or memory usage. Using nested managed object context won't visibly impact the performance of an application if they're used correctly. The option that uses nested managed object contexts is often overlooked or, even worse, most developers don't know this even is an option.
Clarity and Simplicity
A managed object context is very much like a workbench or, as Apple puts it, a scratch pad. By creating a child managed object context that's owned and managed by the add client view controller, we create a separate workbench or scratch pad. We use it to create and work with the Client
instance.
If the user decides it wants to save the contents of the form, the contents of the scratch pad are used to create the Client
instance and, if it passes validation, the Client
instance is pushed to the main managed object context. Does the user change her mind? Then the contents of the scratchpad are tossed in the bin. It's a simple as that.
Other Benefits
There are other, more subtle, benefits. If we add the Client
instance to the main managed object context, we dirty the main managed object context. It can stick around and interfere with other save operations in the application.
Assume, for example, that the Client
instance doesn't pass validation, then that means a save operation of the main managed object context always results in an error, regardless of other managed objects present in the managed object context. This is something we want to avoid.
By using a child managed object context and pushing its changes to its parent managed object context, it is validated. If the Client
instance doesn't pass validation, the save operation of the child managed object context fails and the main managed object context doesn't receive the changes of the child managed object context.
Conclusion
Nested managed object contexts are very often considered a more advanced feature of the Core Data framework. It is true that a simple Core Data stack can go a long way. But I hope you agree that adding a tiny bit of complexity to the mix can result in a big return.
This, and many other patterns, are covered in Core Data Beyond the Basics. If you're new to Core Data, then I suggest you take a look at Core Data Fundamentals.