We successfully seeded the Core Data persistent store with hard-coded seed data in the previous episode. While the implementation works fine, it isn't perfect. Seeding the persistent store takes place on the main thread. We invoke the seed() method in the viewDidLoad() method of the NotesViewController class and we insert the managed objects into the main managed object context of the Core Data manager.

We refactor the implementation in this episode. The goal is to move as much work as possible to a background thread. This should help prevent the application from becoming unresponsive as long as it's seeding the persistent store with data.

Operations

Operations are ideal for performing tasks on a background thread. The idea is straightforward. We create an operation responsible for seeding the persistent store with data and we perform the operation on a background thread.

We won't be using the main managed object context to seed the persistent store with data. We create a private managed object context in the operation with the main managed object context of the Core Data manager as its parent. This is an application of nested managed object contexts, a concept we discuss in detail in Core Data Fundamentals.

Let's get to work. Download the starter project of this episode to follow along. We create a new file in a group named Operations. We choose the Cocoa Touch Class template from the list of iOS > Source templates.

Choose the Cocoa Touch Class File Template

We set Subclass of to Operation to create an Operation subclass. Let's name the class SeedOperation.

Create an Operation Subclass

In SeedOperation.swift, we add an import statement for the Foundation framework and the Core Data framework. The Operation class is defined in the Foundation framework.

import CoreData
import Foundation

class SeedOperation: Operation {

}

The next step is defining a property for the managed object context and a designated initializer for the SeedOperation class. We name the property privateManagedObjectContext and it should be of type NSManagedObjectContext. Notice that the property is a constant private property. Because the privateManagedObjectContext property is defined as a constant, we need to set its value during initialization. We do this in a designated initializer.

import CoreData
import Foundation

class SeedOperation: Operation {

    // MARK: - Properties

    private let privateManagedObjectContext: NSManagedObjectContext

}

The compiler throws an error, which means it's time to implement the designated initializer. The initializer accepts an argument of type NSManagedObjectContext.

import CoreData
import Foundation

class SeedOperation: Operation {

    // MARK: - Properties

    private let privateManagedObjectContext: NSManagedObjectContext

    // MARK: - Initialization

    init(with managedObjectContext: NSManagedObjectContext) {
        // Initialize Managed Object Context
        privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)

        // Configure Managed Object Context
        privateManagedObjectContext.parent = managedObjectContext

        super.init()
    }

}

In the initializer, we create a managed object context by passing privateQueueConcurrencyType to the designated initializer of the NSManagedObjectContext class. We assign the managed object context to the privateManagedObjectContext property.

We set the parent property of the private managed object context to the managed object context that is passed to the initializer of the operation. Last but not least, we invoke the designated initializer of the superclass of SeedOperation.

Seeding Data

Operations perform their work in the main() method. We override this method and invoke seed(), a helper method. I prefer to keep the implementation of the main() method short and simple.

// MARK: - Overrides

override func main() {
    // Seed With Data
    seed()
}

Let's create a stub for the seed() method to keep the compiler happy.

// MARK: - Helper Methods

private func seed() {

}

Before we implement the seed() method, we need to move the hard-coded seed data. We won't burden the CoreDataManager class with this task. Create a new group in the Core Data group and name it Seed Data. Add a new Swift file to this group and name it Seed.swift.

Create a Swift File

In this file, we define a structure, Seed, that is responsible for providing other objects access to the seed data.

import Foundation

struct Seed {

}

Implementing the Seed structure couldn't be easier. We move the seed data from the CoreDataManager class to the Seed structure. The only change we need to make is marking the computed properties as internal by removing the private keywords.

import Foundation

struct Seed {

    // MARK: - Seed Data

    var tags: [String] {
        return [
            "Tips and Tricks",
            ...
            "News"
        ]
    }

    var categories: [(name: String, colorAsHex: String)] {
        return [
            (name: "Core Data",     colorAsHex: "F6D14E"),
            ...
            (name: "Objective-C",   colorAsHex: "4483F3")
        ]
    }

    var notes: [(title: String, createdAt: Double, updatedAt: Double, category: String, tags: [String])] {
        return [
            (title: "Getting Started With Core Data",                               createdAt: 1518011139, updatedAt: 1518011139, category: "Swift",       tags: ["Tips and Tricks"]),
            ...
            (title: "Core Data Fundamentals",                                       createdAt: 1517794761, updatedAt: 1517794761, category: "Swift",       tags: ["News"])
        ]
    }

    var contents: [String] {
        return [
            "Lorem ipsum dolor sit amet ...",
            ...
            "Aliquam sagittis magna felis ..."
        ]
    }

}

The implementation of the seed() method of the SeedOperation class is almost identical to that of the CoreDataManager class. Let's move the seed() method of the CoreDataManager class to the SeedOperation class and find out what needs to change.

Loading Seed Data

Because the seed data is provided by the Seed structure, we start by creating an instance of the Seed structure.

func seed() {
    // Initialize Seed Data
    let seed = Seed()

    ...
}

Any references to the seed data need to be updated because we ask the seed object for the seed data.

func seed() {
    // Initialize Seed Data
    let seed = Seed()

    ...

    for name in seed.tags {
        ...
    }

    for data in seed.categories {
        ...
    }

    for (index, data) in seed.notes.enumerated() {
        ...

        note.contents = seed.contents[index]

        ...
    }
}

The compiler complains that the SeedOperation class doesn't have a mainManagedObjectContext property. We need to replace the references to the mainManagedObjectContext property with references to the privateManagedObjectContext property of the SeedOperation class.

// MARK: - Helper Methods

func seed() {
    // Initialize Seed Data
    let seed = Seed()

    // Helpers
    var tagsBuffer: [Tag] = []
    var categoriesBuffer: [Category] = []

    for name in seed.tags {
        // Initialize Tag
        let tag = Tag(context: privateManagedObjectContext)

        // Configure Tag
        tag.name = name

        // Append to Buffer
        tagsBuffer.append(tag)
    }

    for data in seed.categories {
        // Initialize Category
        let category = Category(context: privateManagedObjectContext)

        // Configure Category
        category.name = data.name
        category.colorAsHex = data.colorAsHex

        // Append to Buffer
        categoriesBuffer.append(category)
    }

    for (index, data) in seed.notes.enumerated() {
        // Initialize Note
        let note = Note(context: privateManagedObjectContext)

        // Configure Note
        note.title = data.title
        note.contents = seed.contents[index]
        note.createdAt = Date(timeIntervalSince1970: data.createdAt)
        note.updatedAt = Date(timeIntervalSince1970: data.updatedAt)

        // Add Category
        note.category = categoriesBuffer.first {
            return $0.name == data.category
        }

        // Helpers
        let tagsAsSet = Set(data.tags)

        // Add Tags
        for tag in tagsBuffer {
            guard let name = tag.name else {
                continue
            }

            if tagsAsSet.contains(name) {
                note.addToTags(tag)
            }
        }
    }

    do {
        // Save Changes
        try privateManagedObjectContext.save()
    } catch {
        print("Unable to Save Main Managed Object Context After Seeding Persistent Store (\(error))")
    }
}

Completion Handler

To make the SeedOperation class easy to use, I'd like to define a completion handler that is invoked when the seed operation has completed. We define a type alias at the top for convenience. The completion handler won't accept any arguments and returns nothing.

import CoreData
import Foundation

class SeedOperation: Operation {

    // MARK: - Type Alias

    typealias SeedOperationCompletion = (() -> Void)

    // MARK: - Properties

    private let privateManagedObjectContext: NSManagedObjectContext

    ...

}

You can adapt this to the needs of your project. You could, for example, pass an optional Error object to the closure if you want to propagate errors that are thrown in the operation. I'm keeping it simple in this example.

We declare a private property, completion, of type SeedOperationCompletion?. Notice that completion is of an optional type. If the consumer of the SeedOperation API isn't interested in the completion of the operation, it can ignore the completion handler.

import CoreData
import Foundation

class SeedOperation: Operation {

    // MARK: - Type Alias

    typealias SeedOperationCompletion = (() -> Void)

    // MARK: - Properties

    private let privateManagedObjectContext: NSManagedObjectContext

    // MARK: -

    private let completion: SeedOperationCompletion?

    ...

}

We pass the completion handler to the designated initializer as an argument. Notice that the default value of the argument is nil. We set the completion property in the initializer before invoking the initializer of the superclass.

// MARK: - Initialization

init(with managedObjectContext: NSManagedObjectContext, completion: SeedOperationCompletion? = nil) {
    // Initialize Managed Object Context
    privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)

    // Configure Managed Object Context
    privateManagedObjectContext.parent = managedObjectContext

    // Set Completion
    self.completion = completion

    super.init()
}

We invoke the completion handler in the main() method, after the seed() method. Because the completion property is of an optional type, we use optional chaining.

// MARK: - Overrides

override func main() {
    // Seed With Data
    seed()

    // Invoke Completion
    completion?()
}

Updating the Core Data Manager

It's time to update the CoreDataManager class and put the SeedOperation class to use. We define a method, seed(_:), that optionally accepts a closure. We pass this closure to the designated initializer of the SeedOperation class.

// MARK: - Public API

func seed(_ completion: (() -> Void)? = nil) {
    // Initialize Operation
    let operation = SeedOperation(with: mainManagedObjectContext, completion: completion)
}

To perform the operation, we need to add it to an operation queue. Let's define an operation queue in the CoreDataManager class. Open CoreDataManager.swift and declare a constant property, operationQueue. We create and assign an OperationQueue instance to the operationQueue property.

import CoreData

final class CoreDataManager {

    // MARK: - Properties

    private let modelName: String

    // MARK: -

    let operationQueue = OperationQueue()

    ...

}

Head back to CoreDataManager+Seed.swift and add the seed operation to the operation queue of the Core Data Manager.

// MARK: - Public API

func seed(_ completion: (() -> Void)? = nil) {
    // Initialize Operation
    let operation = SeedOperation(with: mainManagedObjectContext, completion: completion)

    // Add to Operation Queue
    operationQueue.addOperation(operation)
}

Updating the Notes View Controller

We don't need to update the notes view controller if we don't want to pass a completion handler to the seed(_:) method of the Core Data manager. But let's make sure everything is working by invoking the seed(_:) method with a completion handler and print a message to the console.

// MARK: - View Life Cycle

override func viewDidLoad() {
    ...

    // Seed Persistent Store
    coreDataManager.seed {
        print("Seed Operation Completed")
    }
}

Build and Run

Remove Notes from your device or the simulator, and install a clean build. Because we're using a fetched results controller to populate the table view of the notes view controller, the table view is automatically populated with the seed data the moment the seed operation completes.

Running the Application In the Simulator

We should also see a message in Xcode's console, telling us that the seed operation has completed.

Seed Operation Completed

The completion handler is a nice addition. It allows you, for example, to display a loading indicator as long as the seed operation is running. This may be convenient if you don't want the user to perform certain actions as long as the seed operation is in progress.

What's Next?

Our implementation to seed the persistent store with seed data has improved quite a bit without adding a lot of complexity. The Core Data manager is no longer in charge of seeding the persistent store with data. That's another benefit I like.

Before we replace hard-coded seed data with data loaded from a file or backend, I'd like to make a few small tweaks to avoid duplicate records. This is especially useful in development. Let's do that in the next episode.