Multiple Managed Object Contexts with Core Data Tutorial
Learn how to use multiple managed object contexts to improve the performance of your apps in this Core Data Tutorial in Swift! By Matthew Morey.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Multiple Managed Object Contexts with Core Data Tutorial
35 mins
Editing on a Scratchpad
Right now, SurfJournal uses the main context (coreDataStack.mainContext
) when creating a new journal entry or viewing an existing one. There’s nothing wrong with this approach; the starter project works as-is.
For journaling-style apps like this one, you can simplify the app architecture by thinking of edits or new entries as a set of changes, like a scratch pad. As the user edits the journal entry, you update the attributes of the managed object. Once the changes are complete, you either save them or throw them away, depending on what the user wants to do.
You can think of child managed object contexts as temporary scratch pads that you can either discard completely, or save and send the changes to the parent context.
But what is a child context, technically?
All managed object contexts have a parent store from which you can retrieve and change data in the form of managed objects, such as the JournalEntry
objects in this project. Typically, the parent store is a persistent store coordinator, which is the case for the main context provided by the CoreDataStack
class. Alternatively, you can set the parent store for a given context to another managed object context, making it a child context.
When you save a child context, the changes only go to the parent context. Changes to the parent context won’t be sent to the persistent store coordinator until the parent context is saved.
Before you jump in and add a child context, you need to understand how the current viewing and editing operation works.
Viewing and Editing
The first part of the operation requires segueing from the main list view to the journal detail view. Open JournalListViewController.swift and find prepare(for:sender:)
:
// 1
if segue.identifier == "SegueListToDetail" {
// 2
guard let navigationController =
segue.destination as? UINavigationController,
let detailViewController =
navigationController.topViewController
as? JournalEntryViewController,
let indexPath = tableView.indexPathForSelectedRow else {
fatalError("Application storyboard mis-configuration")
}
// 3
let surfJournalEntry =
fetchedResultsController.object(at: indexPath)
// 4
detailViewController.journalEntry = surfJournalEntry
detailViewController.context =
surfJournalEntry.managedObjectContext
detailViewController.delegate = self
Taking the segue code step-by-step:
- There are two segues: SegueListToDetail and SegueListToDetailAdd. The first, shown in the previous code block, runs when the user taps on a row in the table view to view or edit a previous journal entry.
-
Next, you get a reference to the
JournalEntryViewController
the user is going to end up seeing. It’s presented inside a navigation controller so there’s some unpacking to do. This code also verifies that there’s a selected index path in the table view. -
Next, you get the
JournalEntry
selected by the user, using the fetched results controller’sobject(at:)
method. -
Finally, you set all required variables on the
JournalEntryViewController
instance. ThesurfJournalEntry
variable corresponds to theJournalEntry
entity resolved in step 3. The context variable is the managed object context to be used for any operation; for now, it just uses the main context. TheJournalListViewController
sets itself as the delegate of theJournalEntryViewController
so it can be informed when the user has completed the edit operation.
SegueListToDetailAdd is similar to SegueListToDetail, except the app creates a new JournalEntry
entity instead of retrieving an existing one. The app executes SegueListToDetailAdd when the user taps the plus (+) button on the top-right to create a new journal entry.
Now that you know how both segues work, open JournalEntryViewController.swift and look at the JournalEntryDelegate
protocol at the top of the file:
protocol JournalEntryDelegate {
func didFinish(viewController: JournalEntryViewController,
didSave: Bool)
}
The JournalEntryDelegate
protocol is very short and consists of only one method: didFinish(viewController:didSave:)
. This method, which the protocol requires the delegate to implement, indicates if the user is done editing or viewing a journal entry and whether any changes should be saved.
To understand how didFinish(viewController:didSave:)
works, switch back to JournalListViewController.swift and find that method:
func didFinish(viewController: JournalEntryViewController,
didSave: Bool) {
// 1
guard didSave,
let context = viewController.context,
context.hasChanges else {
dismiss(animated: true)
return
}
// 2
context.perform {
do {
try context.save()
} catch let error as NSError {
fatalError("Error: \(error.localizedDescription)")
}
// 3
self.coreDataStack.saveContext()
}
// 4
dismiss(animated: true)
}
Taking each numbered comment in turn:
Once you add a child context to the workflow later on, the JournalEntryViewController
context will be different from the main context, making this code necessary.
If the save fails, call fatalError
to abort the app with the relevant error information.
-
First, use a
guard
statement to check thedidSave
parameter. This will betrue
if the user taps the Save button instead of the Cancel button, so the app should save the user’s data. Theguard
statement also uses thehasChanges
property to check if anything’s changed; if nothing has changed, there’s no need to waste time doing more work. -
Next, save the
JournalEntryViewController
context inside of aperform(_:)
closure. The code sets this context to the main context; in this case it’s a bit redundant since there’s only one context, but this doesn’t change the behavior.Once you add a child context to the workflow later on, the
JournalEntryViewController
context will be different from the main context, making this code necessary.If the save fails, call
fatalError
to abort the app with the relevant error information. -
Next, save the main context via
saveContext
, defined in CoreDataStack.swift, persisting any edits to disk. -
Finally, dismiss the
JournalEntryViewController
.
If you don’t know what type the context will be, as is the case in didFinish(viewController:didSave:)
, it’s safest to use perform(_:)
so it will work with both parent and child contexts.
MainQueueConcurrencyType
, you don’t have to wrap code in perform(_:)
, but it doesn’t hurt to use it.
If you don’t know what type the context will be, as is the case in didFinish(viewController:didSave:)
, it’s safest to use perform(_:)
so it will work with both parent and child contexts.
There’s a problem with the above implementation — have you spotted it?
When the app adds a new journal entry, it creates a new object and adds it to the managed object context. If the user taps the Cancel button, the app won’t save the context, but the new object will still be present. If the user then adds and saves another entry, the canceled object will still be present! You won’t see it in the UI unless you’ve got the patience to scroll all the way to the end, but it will show up at the bottom of the CSV export.
You could solve this problem by deleting the object when the user cancels the view controller. But what if the changes were complex, involved multiple objects, or required you to alter properties of an object as part of the editing workflow? Using a child context will help you manage these complex situations with ease.