The Elegance of Disposable Managed Object Contexts

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 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 Client Entity

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.

The Invoice Entity

Relationships

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.

Adding a New Client

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 Mastering Core Data With Swift, 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 Mastering Core Data With Swift.

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 Mastering Core Data With Swift.

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By

About Bart Jacobs

About bart jacobs

My name is Bart Jacobs and I run a mobile development company, Code Foundry. I've been programming for more than fifteen years, focusing on Cocoa development soon after the introduction of the iPhone in 2007.

Stop Writing Swift That Sucks

In my free book, you learn the four patterns I use in every Swift project I work on. You learn how easy it is to integrate these patterns in any Swift project.