Modern, Efficient Core Data

In this tutorial, you’ll learn how to improve your iOS app thanks to efficient Core Data usage with batch insert, persistent history and derived properties. By Andrew Tetlaw.

4.5 (13) · 1 Review

Download materials
Save for later
Share

Core Data is one of the venerable Apple Frameworks that’s been around for a long time. Since the release of NSPersistentContainer in iOS 10, Apple has shown Core Data a lot of love. The most recent Core Data additions step it up another huge notch. There are now batch insert requests, persistent history and derived attributes which can definitely make Core Data usage more efficient.

In this tutorial, you’ll improve an app by making the data store much more efficient. You’ll learn how to:

  • Create a batch insert request.
  • Query the persistent store’s transaction history.
  • Control how and when the UI updates in response to new data.

And you just might save the human race in the process!

Note: This intermediate-level tutorial assumes that you have experience building an iOS app using Xcode and writing Swift. You should already have used Core Data and be comfortable with its concepts. If you’d like to learn the basics, you can try our Core Data with SwiftUI tutorial first.

Getting Started

Fireballs! They’re everywhere! Is anyone paying attention? Fireballs could be the first sign of an alien invasion or a portent of a coming Armageddon. Someone has to keep watch. This is your mission. You’ve made an app that downloads fireball sightings from the NASA Jet Propulsion Laboratory (JPL) so you can group them and report on suspicious fireball activity.

Fireball

Download the starter project using the Download Materials button at the top or bottom of this tutorial. Open the starter project. Look at what you have so far.

Exploring Fireball Watch

Build and run the app, so you can get a feel for how it works. The app downloads the latest fireball data from the JPL, creates records for each fireball sighting and stores them in a Core Data stack. You can also create groups and add fireballs to groups for reporting purposes.

When it launches, the list will be empty, so tap the refresh button at the top right of the Fireballs list. Pretty soon, the list should fill up. You can tap it again to see it doesn’t add duplicate records for the same data. If you swipe left on some fireball cells and delete a few, then tap refresh again, you’ll see those fireballs recreated after downloading the data.

Fireball list and details screens

If you tap the Groups tab, you can add a group. Make some groups and then go back to the Fireballs tab and tap a fireball in the list. Then, tap the in-tray button at top right to select one or more groups in which to include that fireball. When you tap a group listed in the Groups tab, it’ll show you a map with all of the fireballs in that group.

Fireball groups list and details

Note: You can read about the JPL fireball API here.

Examining the Core Data Stack

Now take a look at how the app’s Core Data stack is set up.

Open Persistence.swift. You’ll see a class called PersistenceController. This class handles all your Core Data setup and data importing. It uses an NSPersistentContainer which creates a standard SQLite store or, optionally, an in-memory store that’s used for SwiftUI previews.

The persistent container’s viewContext is the managed object context the app uses for the fetch requests that produce the data for the lists. This is a typical setup. You have two entities in your model: Fireball and FireballGroup.

Core Data managed object model

PersistenceController has fetchFireballs() which downloads the fireball data and calls the private importFetchedFireballs(_:) to import the resulting array of FireballData structs as Fireball managed objects. It does this as a background task, using the persistent container’s performBackgroundTask(_:).

importFetchedFireballs(_:) loops through the FireballData array, creates a managed object and saves the managed object context. Because the persistent container’s viewContext has automaticallyMergesChangesFromParent set to true, this might stall the UI while the app saves all the objects. This is a problem which can make an app feel quite clunky and is the target of your first improvement.

Making a Batch Insert Request

The list of reported fireballs will only grow larger, and what if there’s a sudden fireball swarm? A fireball swarm could indicate a possible alien landing site that could herald a new invasion attempt!

Alien invasion!

You want the initial download to be as snappy as possible. Your app needs to quickly get you up to speed with the most current data. Any pauses, delays or hangs are unacceptable — lives depend on it.

Batch inserting comes to the rescue! A batch insert request is a special persistent store request that allows you to import large amounts of data directly into the persistent store. You’ll need a method that creates a batch insert request for this operation. Open Persistence.swift and add the following method to PersistenceController:

private func newBatchInsertRequest(with fireballs: [FireballData])
  -> NSBatchInsertRequest {
  // 1
  var index = 0
  let total = fireballs.count

  // 2
  let batchInsert = NSBatchInsertRequest(
    entity: Fireball.entity()) { (managedObject: NSManagedObject) -> Bool in
    // 3
    guard index < total else { return true }

    if let fireball = managedObject as? Fireball {
      // 4
      let data = fireballs[index]
      fireball.dateTimeStamp = data.dateTimeStamp
      fireball.radiatedEnergy = data.radiatedEnergy
      fireball.impactEnergy = data.impactEnergy
      fireball.latitude = data.latitude
      fireball.longitude = data.longitude
      fireball.altitude = data.altitude
      fireball.velocity = data.velocity
    }

    // 5
    index += 1
    return false
  }
  return batchInsert
}

This method takes the array of FireballData objects and creates an NSBatchInsertRequest to insert them all. Here's how:

  1. You first create local variables to hold the current loop index and the total fireball count.
  2. Create a batch insert request using NSBatchInsertRequest(entity:managedObjectHandler:). This method requires an NSEntity and a closure executed for every insert you want to perform — one for each fireball. The closure must return true if it's your last insert.
  3. Inside the closure, you first check that you've reached the end of the fireballs array and if so return true, completing the request.
  4. This is where you insert new data. The closure is called with an NSManagedObject instance. This is a new object, and after checking that its type is Fireball (it always will be, but you should always be safe), you set the object's properties to match the fetched fireball data.
  5. Finally, you increment the index and return false, indicating that the insert request should call the closure again.
Note: In iOS 13 when NSBatchInsertRequest was first released, there was only one initializer that took an array of dictionaries that represented all of the data to insert. In iOS 14, four new variants were added that used the closure-style initializer and either a managed object or a dictionary for each insertion. See the Apple documentation for more information.