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.