I'm not going to lie. I don't like singletons. Singletons are fine if they're used correctly, but I don't like singletons for convenience. They almost always lead to problems down the line.
This means that the Core Data manager isn't going to be a singleton. We're going to create an instance in the application delegate and inject it into the root view controller. Dependency injection is surprisingly easy if you break it down to its bare essentials.
We first open ViewController.swift and create a property for the Core Data manager. The property is an optional because we set it after the view controller is initialized. That's a drawback I'm happy to accept.
ViewController.swift
import UIKit
class ViewController: UIViewController {
// MARK: - Properties
var coreDataManager: CoreDataManager?
...
}
Next, we open AppDelegate.swift and declare a private constant property, coreDataManager, of type CoreDataManager. We instantiate a CoreDataManager instance and assign it to the property.
AppDelegate.swift
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: - Properties
var window: UIWindow?
// MARK: -
private let coreDataManager = CoreDataManager(modelName: "Notes")
...
}
The magic happens in the application(_:didFinishLaunchingWithOptions:) method. We load the main storyboard and instantiate its initial view controller. We expect the initial view controller to be an instance of the ViewController class. If it isn't, we throw a fatal error.
AppDelegate.swift
// Load Storyboard
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
// Instantiate Initial View Controller
guard let initialViewController = storyboard.instantiateInitialViewController() as? ViewController else {
fatalError("Unable to Configure Initial View Controller")
}
We configure the initial view controller by setting its coreDataManager property. In other words, we inject the Core Data manager into the view controller through property injection. That's all dependency injection is, setting an object's instance variables.
AppDelegate.swift
// Configure Initial View Controller
initialViewController.coreDataManager = coreDataManager
Last but not least, we set the rootViewController property of the window property of the application delegate.
AppDelegate.swift
// Configure Window
window?.rootViewController = initialViewController
This is what the implementation of application(_:didFinishLaunchingWithOptions:) looks like when you're finished.
AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Load Storyboard
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
// Instantiate Initial View Controller
guard let initialViewController = storyboard.instantiateInitialViewController() as? ViewController else {
fatalError("Unable to Configure Initial View Controller")
}
// Configure Initial View Controller
initialViewController.coreDataManager = coreDataManager
// Configure Window
window?.rootViewController = initialViewController
return true
}
Let's give it a try by adding a print statement to the viewDidLoad() method of the ViewController class. Build and run the application and inspect the output in the console.
ViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
print(coreDataManager?.managedObjectContext ?? "No Managed Object Context")
}
This example illustrates how easy it is to use dependency injection to pass objects around without relying on singletons. Truth be told, there's no need to instantiate the Core Data manager in the application delegate, but I hope it shows that singletons aren't the only solution and it certainly shouldn't be your default choice.
Before we move on, I'd like to move the instantiation of the Core Data manager to the view controller. That makes more sense. The application delegate shouldn't be bothered with anything related to Core Data.
ViewController.swift
import UIKit
class ViewController: UIViewController {
// MARK: - Properties
private var coreDataManager = CoreDataManager(modelName: "Notes")
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
}
}
AppDelegate.swift
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: - Properties
var window: UIWindow?
// MARK: - Application Life Cycle
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
return true
}
}
We also have the advantage that the coreDataManager property is no longer an optional. That's a nice benefit. In the next episodes, we take a close look at the data model.