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.
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
Modern, Efficient Core Data
30 mins
- Getting Started
- Exploring Fireball Watch
- Examining the Core Data Stack
- Making a Batch Insert Request
- Batch Inserting Fireballs
- Enabling Notifications
- Enabling Persistent History Tracking
- Making a History Request
- Revealing a Conundrum: Big Notifications
- Step 1: Setting a Query Generation
- Step 2: Saving the History Token
- Step 3: Loading the History Token
- Step 4: Setting a Transaction Author
- Step 5: Creating a History Request Predicate
- Step 6: Merging Important Changes
- Adding Derived Attributes
- Where to Go From Here?
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!
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.
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.
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.
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
.
PersistenceController
has fetchFireballs()
which downloads the fireball data and calls the private importFetchedFireballs(_:)
to import the resulting array of FireballData struct
s 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!
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:
- You first create local variables to hold the current loop index and the total fireball count.
- Create a batch insert request using
NSBatchInsertRequest(entity:managedObjectHandler:)
. This method requires anNSEntity
and a closure executed for every insert you want to perform — one for each fireball. The closure must returntrue
if it's your last insert. - Inside the closure, you first check that you've reached the end of the fireballs array and if so return
true
, completing the request. - 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 isFireball
(it always will be, but you should always be safe), you set the object's properties to match the fetched fireball data. - Finally, you increment the index and return
false
, indicating that the insert request should call the closure again.
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.