Building The Perfect Core Data Stack

Passing It Around

Building The Perfect Core Data Stack

The Core Data stack we've built in this series is slowly taking shape. With every iteration, we add a dash of complexity in return for a few key advantages.

The Core Data stack is set up and managed by the CoreDataManager class. It provides access to the main managed object context, neatly hiding the private managed object context, the managed object model, and the persistent store coordinator from the rest of the application.

One Instance to Rule Them All

The majority of applications need only one Core Data stack, which implies that a single instance of the CoreDataManager class suffices. At the mention of the word single, a surprising number of developers have the urge to turn the CoreDataManager instance into a singleton. While I don't have any objections against the singleton pattern, I have a strong opinion about the motivation for using this controversial pattern.

"In software engineering, the singleton pattern is a design pattern that restricts the instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system."— Wikipedia

Does the above definition of the singleton pattern surprise you? Many developers create singletons for a different reason, that is, providing easy access to the singleton object. This, however, is a byproduct of the singleton pattern. It isn't the goal of the singleton pattern.

By easy access I mean of course global access. That's the motivation many developers have when using the singleton pattern. But what is wrong with a globally accessible object?

While I don't want to dive into the details of why globals are a bad idea, I do want to emphasize the drawbacks of the singleton pattern when used incorrectly, that is, for global access to a single object.

Making an object globally accessible instead of passing it around is considered an anti-pattern. Whenever you're about to create a singleton, consider why it's necessary to make it a globally accessible object. Is it convenience? Is there an alternative approach that also solves the problem?

Another reason for not turning the CoreDataManager instance into a singleton is dependency management. Globally accessible objects that are used in various places of a project obfuscate the dependencies of the project. A much better approach is to use dependency injection. I've written several articles about dependency injection because I really enjoy the simplicity and transparency of this pattern. It's much easier than most developers think. I like to quote James Shore whenever I discuss dependency injection.

"Dependency injection is a 25-dollar term for a 5-cent concept." — James Shore

Injecting the Core Data Manager

In this episode, I show you how easy it is to make the CoreDataManager instance accessible in other parts of the application without turning to the singleton pattern. Remember that we created the CoreDataManager instance in the project's AppDelegate class. Revisit the project from the previous episode if you need to refresh your memory.

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    // MARK: - Properties

    var window: UIWindow?

    // MARK: -

    private let coreDataManager = CoreDataManager(modelName: "DataModel")

    // MARK: - Application Life Cycle

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        print(coreDataManager.mainManagedObjectContext)
        return true
    }

    ...

}

How do we pass the Core Data manager of the application delegate to the ViewController instance, the initial view controller of the storyboard? It's tempting to create a singleton and access it with a convenience method. We can also resolve this problem with dependency injection.

We start by declaring a property for the CoreDataManager instance in the ViewController class. The first benefit of this approach immediately becomes apparent. By declaring an internal property for the CoreDataManager class, we explicitly define the CoreDataManager class as a dependency of the ViewController class. Notice that the coreDataManager property is of type CoreDataManager?, an optional.

import UIKit

class ViewController: UIViewController {

    // MARK: - Properties

    var coreDataManager: CoreDataManager?

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

It's also possible, and more convenient, to declare the coreDataManager property as an implicitly unwrapped optional. If the coreDataManager property is nil when the view controller tries to access it, then we have bigger problems to worry about.

We set the coreDataManager property in the application(_:didFinishLaunchingWithOptions:) method of the AppDelegate class. We slightly refactor the application(_:didFinishLaunchingWithOptions:) method as you can see below.

// MARK: - Application Life Cycle

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    guard let viewController = window?.rootViewController as? ViewController else {
        fatalError("Unexpected Root View Controller")
    }

    // Configure View Controller
    viewController.coreDataManager = coreDataManager

    return true
}

We access the root view controller of the application window and cast it to an instance of the ViewController class. If that operation fails, then a fatal error is thrown. This is fine because that should never happen.

We set the coreDataManager property of the ViewController instance. This is better known as property injection, a type of dependency injection.

// Configure View Controller
viewController.coreDataManager = coreDataManager

To prove that everything works as advertised, update the implementation of the viewDidLoad() method of the ViewController class. Run the application in the simulator and inspect the output in the console.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    print(coreDataManager?.mainManagedObjectContext)
}

Are We Jumping Through Hoops?

It seems as if we're unnecessarily jumping through hoops. If the above arguments haven't convinced you of avoiding singletons for the purpose of easy access, then consider testing. By injecting the CoreDataManager instance into the ViewController class, testing becomes much easier. We could, for example, replace the CoreDataManager instance with a mock object with very little effort.

Whenever I write about singletons, I emphasize that the singleton pattern isn't a code smell per se. Using the singleton pattern to provide global access to the singleton object is. The next time you're about to create a singleton, give dependency injection a try. You'll be surprised by how easy it is to adopt.

Next Episode "Give It Time"