Adding Core Data to an application

When you create a new project in Xcode, Xcode asks whether you want to add Core Data to your application. If you check this checkbox, Xcode will automatically generate some boilerplate code that sets up the Core Data stack. Prior to iOS 10, this boilerplate code spanned a couple of dozen lines because it had to load the data model, connect to the persistent store, and then it also had to set up the managed object context.

In iOS 10, Apple introduced NSPersistentContainer. When you initialize an NSPersistentContainer, all this boilerplate code is obsolete and the hard work is done by the NSPersistentContainer. This results in much less boilerplate code to obtain a managed object context for your application. Let's get started with setting up your Core Data stack. Open AppDelegate.swift and add the following import statement:

import CoreData 

Next, add the following lazy variable to the implementation of AppDelegate:

private lazy var persistentContainer: 
NSPersistentContainer = {
let container = NSPersistentContainer(name:
"FamilyMovies")
container.loadPersistentStores(completionHandler: {
(storeDescription, error) in
if let error = error {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
If you declare a variable as lazy, it won't be initialized until it is accessed. This is particularly useful for variables that are expensive to initialize, rely on other objects, or are not always accessed. The fact that the variable is initialized just in time comes with a performance penalty since the variable needs to be set up the first time you access it. In certain cases this is fine, but in other cases it might negatively impact the user experience. When used correctly, lazy variables can have great benefits.

The preceding snippet does all the work required to load the data model we'll create shortly. It also connects this data model to a persistent store and initializes our managed object context.

If you let Xcode generate the Core Data code for your app, a method called saveContext is added to AppDelegate as well. This method is used in applicationWillTerminate(_:) to perform a last-minute save of changes and updates when the application is about to terminate. Since we're setting up Core Data manually, we will also add this behavior. However, instead of placing the saveContext method in AppDelegate, we will add this method as an extension to the NSPersistentContainer. This makes easier for other parts of your code to use this method, without relying on AppDelegate.

Create a new folder in the Project navigator and name it Extensions. Also, create a new Swift file and name it NSPersistentContainer.swift. Add the following implementation to this file:

import CoreData 

extension NSPersistentContainer {
func saveContextIfNeeded() {
if viewContext.hasChanges {
do {
try viewContext.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}

The preceding code adds a custom method to NSPersistentContainer so that we can call on instances of the NSPersistentContainer. This is really convenient because it decouples this method from AppDelegate entirely. Add the following implementation of applicationWillTerminate(_:) to AppDelegate to save the context right before the app terminates:

func applicationWillTerminate(_ application: UIApplication) { 
persistentContainer.saveContextIfNeeded()
}

Now, whenever the application terminates, the persistent store will check whether there are any changes to the managed object context that the viewContext property points to. If there are any changes, we'll attempt to save them. If this fails, we crash the app using fatalError. In your own app, you might want to handle this scenario a bit more gracefully. It could very well be that saving before termination isn't something that you want to crash your app on if it fails. You can modify the error handling implementation of saveContextIfNeeded if you think a different behavior is more appropriate for your app.

Now that we have our Core Data stack set up, we need to make it available to the app's view controllers. We will make this happen by using a from of dependency injection. This means that our AppDelegate will pass the managed object context to our initial view controller. It will then be this view controller's job to pass the context on to other view controllers that might need the managed object context as well.

First, add a property to FamilyMembersViewController that holds the managed object context. Don't forget to import Core Data at the top of this view controller:

var managedObjectContext: NSManagedObjectContext? 

Now, in AppDelegate, modify the application(_:, didFinishLaunchingWithOptions:) method as follows:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { 

if let navVC = window?.rootViewController as?
UINavigationController,
var initialVC = navVC.viewControllers[0] as?
MOCViewControllerType {

initialVC.managedObjectContext =
persistentContainer.viewContext
}

return true
}

Even though this code does exactly what it should, we can still make one major improvement. We know that there will be more view controllers that will hold on to a managed object context, and checking for classes is something we'd rather not do. As an exercise, attempt to fix this issue by adding a protocol called MOCViewControllerType. This protocol should add a requirement for an optional managedObjectContext property. Make sure that FamilyMembersViewController conforms to this protocol and fix the implementation of application(_:, didFinishLaunchingWithOptions:) as well.

The implemented code has layed out the base for our application's Core Data stack. We initialized the stack, and the managed object context is injected into the app's main view controller through a protocol. Let's see how we can define and set up our models.