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
Doing Work in the Background
If you haven’t done so already, tap the Export button at the top-left and then immediately try to scroll the list of surf session journal entries. Notice anything? The export operation takes several seconds, and it prevents the UI from responding to touch events such as scrolling.
The UI is blocked during the export operation because both the export operation and UI are using the main queue to perform their work. This is the default behavior.
The traditional way to fix this is to use Grand Central Dispatch to run the export operation on a background queue. However, Core Data managed object contexts are not thread-safe. That means you can’t just dispatch to a background queue and use the same Core Data stack.
The solution is simple: use a private background queue rather than the main queue for the export operation. This will keep the main queue free for the UI to use.
But before you jump in and fix the problem, you need to understand how the export operation works.
Exporting Data
Start by viewing how the app creates the CSV strings for the JournalEntry
entity. Open JournalEntry+Helper.swift and find csv()
:
func csv() -> String {
let coalescedHeight = height ?? ""
let coalescedPeriod = period ?? ""
let coalescedWind = wind ?? ""
let coalescedLocation = location ?? ""
let coalescedRating: String
if let rating = rating?.int32Value {
coalescedRating = String(rating)
} else {
coalescedRating = ""
}
return "\(stringForDate()),\(coalescedHeight),\(coalescedPeriod),\(coalescedWind),\(coalescedLocation),\(coalescedRating)\n"
}
As you can see, JournalEntry
returns a comma-separated string of the entity’s attributes. Because the JournalEntry
attributes are allowed to be nil
, the function uses the nil coalescing operator (??
) to export an empty string instead of an unhelpful debug message that the attribute is nil
.
??
) unwraps an optional if it contains a value; otherwise it returns a default value. For example, the following: let coalescedHeight = height != nil ? height! : ""
can be shortened using the nil coalescing operator to: let coalescedHeight = height ?? ""
.
That’s how the app creates the CSV strings for an individual journal entry, but how does the app save the CSV file to disk? Open JournalListViewController.swift and find the following code in exportCSVFile()
:
// 1
let context = coreDataStack.mainContext
var results: [JournalEntry] = []
do {
results = try context.fetch(self.surfJournalFetchRequest())
} catch let error as NSError {
print("ERROR: \(error.localizedDescription)")
}
// 2
let exportFilePath = NSTemporaryDirectory() + "export.csv"
let exportFileURL = URL(fileURLWithPath: exportFilePath)
FileManager.default.createFile(atPath: exportFilePath,
contents: Data(), attributes: nil)
Going through the CSV export code step-by-step:
The fetch request is the same one used by the fetched results controller. Therefore, you reuse the surfJournalFetchRequest
method to create the request to avoid duplication.
The path returned by NSTemporaryDirectory
is a unique directory for temporary file storage. This a good place for files that can easily be generated again and don’t need to be backed up by iTunes or to iCloud.
After creating the export URL, call createFile(atPath:contents:attributes:)
to create the empty file where you’ll store the exported data. If a file already exists at the specified file path, this method will remove it first.
-
First, retrieve all
JournalEntry
entities by executing a fetch request.The fetch request is the same one used by the fetched results controller. Therefore, you reuse the
surfJournalFetchRequest
method to create the request to avoid duplication. -
Next, create the URL for the exported CSV file by appending the file name (“export.csv”) to the output of the
NSTemporaryDirectory
method.The path returned by
NSTemporaryDirectory
is a unique directory for temporary file storage. This a good place for files that can easily be generated again and don’t need to be backed up by iTunes or to iCloud.After creating the export URL, call
createFile(atPath:contents:attributes:)
to create the empty file where you’ll store the exported data. If a file already exists at the specified file path, this method will remove it first.
Once the app has the empty file, it can write the CSV data to disk:
// 3
let fileHandle: FileHandle?
do {
fileHandle = try FileHandle(forWritingTo: exportFileURL)
} catch let error as NSError {
print("ERROR: \(error.localizedDescription)")
fileHandle = nil
}
if let fileHandle = fileHandle {
// 4
for journalEntry in results {
fileHandle.seekToEndOfFile()
guard let csvData = journalEntry
.csv()
.data(using: .utf8, allowLossyConversion: false) else {
continue
}
fileHandle.write(csvData)
}
// 5
fileHandle.closeFile()
print("Export Path: \(exportFilePath)")
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
self.showExportFinishedAlertView(exportFilePath)
} else {
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
}
Here’s how the file-handling works:
-
First, the app needs to create a file handler for writing, which is simply an object that handles the low-level disk operations necessary for writing data. To create a file handler for writing, use the
FileHandle(forWritingTo:)
initializer.
During each iteration, you attempt to create a UTF8-encoded string using csv()
on JournalEntry
and data(using:allowLossyConversion:)
on String
.
If it’s successful, you write the UTF8 string to disk using the file handler write()
method.
-
Next, iterate over all
JournalEntry
entities.During each iteration, you attempt to create a UTF8-encoded string using
csv()
onJournalEntry
anddata(using:allowLossyConversion:)
onString
.If it’s successful, you write the UTF8 string to disk using the file handler
write()
method. - Finally, close the export file-writing file handler, since it’s no longer needed.
Once the app has written all the data to disk, it shows an alert dialog with the exported file path.
UIActivityViewController
.
To open the exported CSV file, use Excel, Numbers or your favorite text editor to navigate to and open the file specified in the alert dialog. If you open the file in Numbers you will see the following:
Now that you’ve seen how the app currently exports data, it’s time to make some improvements.
Exporting in the Background
You want the UI to continue working while the export is happening. To fix the UI problem, you’ll perform the export operation on a private background context instead of on the main context.
Open JournalListViewController.swift and find the following code in exportCSVFile()
:
// 1
let context = coreDataStack.mainContext
var results: [JournalEntry] = []
do {
results = try context.fetch(self.surfJournalFetchRequest())
} catch let error as NSError {
print("ERROR: \(error.localizedDescription)")
}
As you saw earlier, this code retrieves all of the journal entries by calling fetch()
on the managed object context.
Next, replace the above code with the following:
// 1
coreDataStack.storeContainer.performBackgroundTask { context in
var results: [JournalEntry] = []
do {
results = try context.fetch(self.surfJournalFetchRequest())
} catch let error as NSError {
print("ERROR: \(error.localizedDescription)")
}
Instead of using the main managed object context also used by the UI, you’re now calling performBackgroundTask(_:)
on the stack’s persistent store container. This creates a new managed object context and passes it into the closure.
The context created by performBackgroundTask(_:)
is on a private queue, which doesn’t block the main UI queue. The code in the closure is run on that private queue. You could also manually create a new temporary private context with a concurrency type of .privateQueueConcurrencyType
instead of using performBackgroundTask(_:)
.
Private Queue specifies the context that will be associated with a private dispatch queue instead of the main queue. This is the type of queue you just used to move the export operation off of the main queue so it would no longer interfere with the UI.
Main Queue, the default type, specifies that the context will be associated with the main queue. This type is what the main context (coreDataStack.mainContext
) uses. Any UI operation, such as creating the fetched results controller for the table view, must use a context of this type.
Contexts and their managed objects must only be accessed from the correct queue. NSManagedObjectContext
has perform(_:)
and performAndWait(_:)
to direct work to the correct queue. You can add the launch argument -com.apple.CoreData.ConcurrencyDebug 1
to your app’s scheme to catch mistakes in the debugger.
Private Queue specifies the context that will be associated with a private dispatch queue instead of the main queue. This is the type of queue you just used to move the export operation off of the main queue so it would no longer interfere with the UI.
Main Queue, the default type, specifies that the context will be associated with the main queue. This type is what the main context (coreDataStack.mainContext
) uses. Any UI operation, such as creating the fetched results controller for the table view, must use a context of this type.
Contexts and their managed objects must only be accessed from the correct queue. NSManagedObjectContext
has perform(_:)
and performAndWait(_:)
to direct work to the correct queue. You can add the launch argument -com.apple.CoreData.ConcurrencyDebug 1
to your app’s scheme to catch mistakes in the debugger.
Next, find the following code in the same method:
print("Export Path: \(exportFilePath)")
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
self.showExportFinishedAlertView(exportFilePath)
} else {
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
}
Replace the code with the following:
print("Export Path: \(exportFilePath)")
// 6
DispatchQueue.main.async {
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
self.showExportFinishedAlertView(exportFilePath)
}
} else {
DispatchQueue.main.async {
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
}
}
} // 7 Closing brace for performBackgroundTask
To finish off the task:
-
You should always perform all operations related to the UI on the main queue, such as showing an alert view when the export operation is finished; otherwise, unpredictable things might happen. Use
DispatchQueue.main.async
to show the final alert view message on the main queue. -
Finally, add a closing curly brace to close the block you opened earlier in step 1 via the
performBackgroundTask(_:)
call.
Now that you’ve moved the export operation to a new context with a private queue, build and run to see if it works!
You should see exactly what you saw before:
Tap the Export button in the top left, and immediately try to scroll the list of surf session journal entries. Notice anything different this time? The export operation still takes several seconds to complete, but the table view continues to scroll during this time. The export operation is no longer blocking the UI.
Cowabunga, dude! Gnarly job making the UI more responsive.
You’ve just witnessed how doing work on a private background queue can improve a user’s experience with your app. Now you’ll expand on the use of multiple contexts by examining a child context.