Leave a rating/review
Welcome back! In the last episode, you created BatchInsertRequests to get your SpaceXLaunch information into the database in one request. But how can you do this so it doesn’t interfere with the main queue where user interactions take place? Long running or CPU intensive operations can be done in the background. Core Data has had support for background operations for a while now, but with iOS 15 they adopted the asynchronous API that was introduced throughout other frameworks. Let’s see how you can combine past and present to get your data stored and be performant at the same time.
Open up the starter project for this episode. I want to point out a few changes since last episode. The SpaceX API allows launches to appear in multiple lists (Past and All launches for example), so we need to update the data model and the code.
Starting with the data model, the RocketLaunch entity’s list relationship is now a “to-many” relationship. Then in RocketLaunch+CoreDataProperties, in the createWith function, the code has been updated when setting the list
launch.addToList(list)
Then in launches(in list) the list predicate’s format string now contains the ANY keyword
"ANY %K == %@"
This searches for the title of ANY of the lists the launch has.
Finally, to make all this work, the list property declaration is now a Set of RocketLaunches
@NSManaged public var list: Set<RocketLaunchList>
There are also some generated accessors at the bottom of the file (where the addToList function is defined).
With that in place, now onto swift concurrency!
If you’re going to be using background threads to insert data into your store, you would ideally want a separate context to work with in that thread. This keeps you from interfering with the context hooked up to the user interface, which is listening for changes. Once the background context is done with its changes, it can merge those into the store. It’s easy enough to make a background context. Above the importLaunches method, add a method to get a new context:
private func newTaskContext() -> NSManagedObjectContext {
let taskContext = container.newBackgroundContext()
taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return taskContext
}
The NSPersistentContainer class has a newBackgroundContext() method that is used to fetch the new context. The merge policy is set to NSMergeByPropertyObjectTrumpMergePolicy, which merges the conflicts between the persistent store’s version of an object and the in-memory version by individual property, with the in-memory changes trumping external changes.
With this in place, we can replace the context you declared in the importLaunches
let taskContext = newTaskContext()
taskContext.name = "importContext"
taskContext.transactionAuthor = "importLaunches"
Here, the context is created and also named, and has an author assigned to it. This is because you can have multiple background contexts! We’re going to keep it simple though, and stick with one background context.
Now it’s time to wrap the calls you made in the last episode in a call to the context so it will execute in the background. There are a few ways to do this. You could place this around the code that batch inserts the launches:
container.performBackgroundTask { context in
}
The call to performBackgroundTask provides a new background context to the closure, and carries out whatever is in the body of that closure. Using this technique, you could have multiple contexts in play, all doing things at the same time. We want to be a little more controlled with what we’re doing, so replace that code with a call to the performAndWait method instead:
try taskContext.performAndWait {
// Use a batch insert request to add the launches
let batchInsertRequest = createBatchInsertLaunchRequest(from: launchCollection)
if let fetchResult = try?
taskContext.execute(batchInsertRequest),
let batchInsertResult = fetchResult as? NSBatchInsertResult,
let success = batchInsertResult.result as? Bool,
success {
return
}
throw LaunchError.batchInsertError
}
There is also a slight reformatting of the return case and the throwing of the potential error. Since this code is executed in the closure on the background thread and the control flow is different than the surrounding code, if it succeeds, we can simply return. If any of the checks in the if let fail, it will immediately throw the batchInsertError.
But why performAndWait instead of just perform? Here, you want to make sure that the batch insert of the launches completes before the relationship connections are established. For the links and fairings, we can use perform since there is less data to deal with, and can always fall back to performAndWait if needed.
Update the fetch for the lists using performAndWait:
var list: SpaceXLaunchList!
try taskContext.performAndWait {
let fetchRequest = SpaceXLaunchList.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "title == %@", listName)
let results = try taskContext.fetch(fetchRequest)
if let fetchedList = results.first {
list = fetchedList
}
}
Wrap the block of code that deals with the fairing relationships in the same way
try taskContext.perform {
// Use a batch insert request to add the fairings
let batchInsertRequest = createBatchInsertRelationshipRequest(from: fairings, for: SpaceXFairings.self)
if let fetchResult = try? taskContext.execute(batchInsertRequest),
let batchInsertResult = fetchResult as? NSBatchInsertResult,
let success = batchInsertResult.result as? Bool, success {
return
}
throw LaunchError.batchInsertError
}
Again, the code is wrapped with try taskContext.perform and the error is handled differently from before since it’s in the closure. You might have noticed that the code no longer compiles - you’ll address that in a minute. Repeat this process for the fairings for loop, and the 2 SpaceXLinks code blocks:
try taskContext.perform {
//around links batch insert
}
try await taskContext.perform {
//around links for loop
}
OK, now to fix this compile error. If you look at one of the errors, you’ll see that it complains that taskContext.perform is an async call in a function that doesn’t support concurrency. That’s right! taskContext.perform and some of its siblings have been adopted to use Swift concurrency. This means that they must use await when called, and be inside a function that is labeled async. Change the try taskContext.perform to use await:
try await taskContext.perform
Then go to the importLaunches function signature and have it declare it is async
func importLaunches(from launchCollection: [SpaceXLaunchJSON], to listName: String) async throws
A new set of compile errors shows that you have to make similar modifications in fetchSpaceXLaunches and have the calls to importLaunches use await
try await PersistenceController.shared.importLaunches(from: launches, to: "All")
The fetchSpaceXLaunches function was already marked async since the calls to the SpaceXAPI class needed that.
Before running, I forgot one more save for the context near the bottom.
Ok, build and run the app. When the app loads, tap the reload button and behind the scenes the launches will load into each of the lists. Tap into the “All launches” list to see the complete list of launches. Tap into a detail view for one of the launches and you can see much more information than the manual launch entries, including the mission logo and the Reddit pages - both of which were found in the SpaceXLinks part of the JSON. Great! Everything seems to be in place.
In the next video, you’ll learn how to deal with deleting entries in Core Data - which is not always obvious when relationships are involved. See you then!