Core Data: Beyond the Basics

Jul 26 2022 · Swift 5.5, iOS 15, Xcode 13.3.1

Part 2: Advanced Core Data

15. Saving Launches with Batch Operations

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 14. Asynchronously Loading Launches Next episode: 16. Saving Launches Concurrently

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 15. Saving Launches with Batch Operations

Now that you have data from the SpaceX API, it’s time to put that data in the database. There are two techniques you’ll use together to do this: one is batch operations, which you’ll learn about in this episode, and asynchronous Core Data, which you’ll learn about in the next episode.

Batch insertions do just that - they insert a set of values into the database as a single transaction. To do this, you’ll need a BatchInsertRequest. This request is then sent to a context.

But which context? In addition to support for creating background contexts, which has been around since iOS 10, iOS 15 introduced asynchronous API into Core Data that enable you to perform operations in an async/await manner. Let’s go to the demo and set up the batch insertion code first.

Open the Starter project for this episode and open Persistence.swift. If you’ve been following along to this point, you’ll see some additions to this file since the last episode:

  • The preview context now has some dummy data to use, mimicking the data from the SpaceXAPI feed.

Highlight that area of the code (right near the top)

  • A fetchSpaceXLaunches() function grabs the data for particular URLs and stores them into lists in the database.

Highlight that area of the code

  • A createSpaceXLaunchLists() function creates some SpaceX launch lists and stores them in the database. This function uses some techniques you’ll learn about shortly.

Highlight that area of the code

  • The getAllLists and getTestLaunch functions do just what they say in their names.

Highlight that area of the code

  • Finally, a place holder for importLaunches has been added so the code compiles.

With that out of the way, let’s add some code to perform batch insertions. The signature for importLaunches takes in an array of SpaceXLaunch objects. You want to add them to the database, but instead of doing each operation independently, you can use a batch operation to add all of them at once. It sounds like you need some supporting functions for importLaunches. Define the function for createBatchInsertLaunchRequest right below importLaunches - it will take in that same array. It will return a NSBatchInsertRequest:

private func createBatchInsertLaunchRequest(from launchCollection: [SpaceXLaunchJSON]) -> NSBatchInsertRequest {

}

You’ll need to have an index to track which element the closure is currently on:

var index = 0
let total = launchCollection.count

Then define and return the NSBatchInsertRequest. It will take in an entity and a dictionary handler as an argument:

let batchInsertRequest = NSBatchInsertRequest(entity:  
  SpaceXLaunch.entity(), dictionaryHandler: { dictionary in
  guard index < total else { return true }
  dictionary.addEntries(from: launchCollection[index].dictionaryValue as [AnyHashable: Any])
    index += 1
    return false
  })
return batchInsertRequest

What’s going on in this code block? The request takes in the SpaceXLaunch entity, which is the type of objects in the array. In the handler, as long as index is less than total, the launch’s dictionaryValue elements get added to the closure’s dictionary, and the index is ticked up by one. But wait - where did this dictionaryValues come from?

Most of the model types in the starter project already have this field added - but not SpaceXFairings. Add that computed property:

var dictionaryValue: [String: Any] {
	[
	  "reused": reused as Any,
	  "recoveryAttempt": recoveryAttempt as Any,
	  "recovered": recovered as Any,
	  "ships": ships,
	  "id": id
	]
}

This assigns a key to each field of the struct that the batch insert request can use to access the fields.

In preparation for some work you’ll do with relationships later on, make a protocol in the SpaceXFairings file called BatchInsertable:

protocol BatchInsertable: Codable {
  var dictionaryValue: [String: Any] { get }
}

Then go and change each of the model structs in the SpaceXFairings, SpaceXLinks and SpaceXLaunchJSON files to adopt this protocol instead of just Codable. You’ll use this in a bit.

I will do this for each struct in the demo.

Go back to importLaunches and start to add code. You’ll add code in this video and the next, so the app won’t run quite yet.

Start by getting the container’s viewContext:

let taskContext = container.viewContext

As you know you can perform operations on the context to load data from and save data to the persistent store.

Then, get an array of tuples that associate the id of the launch with the associated fairings object. Do the same for the links.

let fairings = launchCollection.map { ($0.id, $0.fairings) }
let links = launchCollection.map { ($0.id, $0.links) }

This will help store the relationships when performing the batch insert operations.

Now perform some operations to start populating the data. First, get the list that matches the list name passed into the function:

var list: SpaceXLaunchList!
let fetchRequest = SpaceXLaunchList.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "title == %@", listName)
let results = try taskContext.fetch(fetchRequest)
if let fetchedList = results.first {
	list = fetchedList
}

Note that you’re not doing anything special with asynchronous code or background contexts, at least not yet. This code should look fairly straight forward to you by now - getting a fetch request for all the SpaceXLaunchLists, setting the predicate, and then running that fetch on the context to hopefully get a result.

Next, use the batch insert method you created earlier to add those entires to the database.

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
	} else {
		throw LaunchError.batchInsertError
	}

This again looks very familiar. You make the request, execute it on the context, and then go through a series of checks to make sure that there are valid results. The return in the success block doesn’t make sense right now, but it will in the next video. Hold on tight!

Now with the launches in place, we need to establish some of the other relationships. Here, you’ll focus on the links and the fairings, since you need the links in the app, and the fairings are there for good measure. Make a variation on the createBatchInsertLaunchRequest function that can handle the relationship arrays you made earlier:

private func createBatchInsertRelationshipRequest<T: BatchInsertable, E: NSManagedObject>(from relationshipCollection: [(String, T?)], for type: E.Type) -> NSBatchInsertRequest {
	var index = 0
	let total = relationshipCollection.count
	
	// Provide one dictionary at a time when the closure is called.
	let batchInsertRequest = NSBatchInsertRequest(entity: E.entity(), dictionaryHandler: { dictionary in
	  guard index < total else { return true }
	  guard let value = relationshipCollection[index].1 else { index += 1; return false }
	  dictionary.addEntries(from: value.dictionaryValue as [AnyHashable: Any])
	  index += 1
	  return false
	})
	
	return batchInsertRequest
}

Here, you’re using generics to handle relationships collections that have members that adopt BatchInsertable and for types that adopt to NSManagedObject. The request is built in a similar fashion as before, except this time you peek into the relationship array, grab the second entry of the pairing (the .1 on the tuple) and add its dictionary values in.

Other relationships can be done in the same way.

Now with that functionality in place, you can deal with the fairings and the links.

let batchInsertRequest2 = createBatchInsertRelationshipRequest(from: fairings, for: SpaceXFairings.self)
if let fetchResult = try? taskContext.execute(batchInsertRequest2),
let batchInsertResult = fetchResult as? NSBatchInsertResult,
let success = batchInsertResult.result as? Bool, success {
  return
} else {
throw LaunchError.batchInsertError
}

This drills down through the launches fairings, and batch inserts those into the database. Now establish connections between the launches and the fairings:

// Setup the fairing relationships
for (id, fairing) in fairings {
	guard let fairing = fairing else { continue }
	let fairingFetchRequest = SpaceXFairings.fetchRequest()
	fairingFetchRequest.predicate = NSPredicate(format: "id == %@", argumentArray: [fairing.id])
	
	let launchFetchRequest = SpaceXLaunch.fetchRequest()
	launchFetchRequest.predicate = NSPredicate(format: "id == %@", argumentArray: [id])
	
	let returnedFairing = try taskContext.fetch(fairingFetchRequest) as [SpaceXFairings]
	let launch = try taskContext.fetch(launchFetchRequest) as [SpaceXLaunch]
	guard !returnedFairing.isEmpty, !launch.isEmpty else { continue }
	let matchedFairing = returnedFairing[0]
	let matchedLaunch = launch[0]
	matchedFairing.launch = matchedLaunch
}
try taskContext.save()

Once the fairings are matched to the appropriate launches, you can call save on the context to push them to the persistent store.

Do the same thing for the links.

// Use a batch insert request to add the links
let batchInsertRequest3 = createBatchInsertRelationshipRequest(from: links, for: SpaceXLinks.self)
if let fetchResult = try? taskContext.execute(batchInsertRequest3),
let batchInsertResult = fetchResult as? NSBatchInsertResult,
let success = batchInsertResult.result as? Bool, success {
  return
} else {
	throw LaunchError.batchInsertError
}

// Setup the link relationships
for (id, links) in links {
	let linksFetchRequest = SpaceXLinks.fetchRequest()
	linksFetchRequest.predicate = NSPredicate(format: "id == %@", argumentArray: [links.id])
	
	let launchFetchRequest = SpaceXLaunch.fetchRequest()
	launchFetchRequest.predicate = NSPredicate(format: "id == %@", argumentArray: [id])
	
	let returnedLinks = try taskContext.fetch(linksFetchRequest) as [SpaceXLinks]
	let launch = try taskContext.fetch(launchFetchRequest) as [SpaceXLaunch]
	guard !returnedLinks.isEmpty, !launch.isEmpty else { continue }
	returnedLinks[0].launch = launch[0]
	launch[0].addToSpaceXList(list)
}
try taskContext.save()

As mentioned before this won’t work just yet. If you read through this method there is a lot going on, and some of the processes may take some time to finish. There are some things you want to happen first, such as inserting the links, before processing the links. Also, all this is likely to jam up the user interface while the app does work. It sounds like some work in the background is needed, and possibly some ordered work at that. In the next video, you’ll learn how to do that. This was a lot to digest, so take a quick break, and I’ll see you at the next video.