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?
Revealing a Conundrum: Big Notifications
This reveals a conundrum, and if you've already noticed it, well done!
Any change to the persistent store triggers a notification, even if your user adds or deletes a managed object from a user interaction. That's not all: Notice that your history fetch request also returns all changes from the beginning of the transaction log.
Your notifications are too big!
Your intention is to avoid doing any unnecessary work for the view context, taking control of when to refresh the view context. No problem at all, you have it covered. To make the whole process clear, you'll do this in a few easy-to-follow steps.
Step 1: Setting a Query Generation
The first step — a small one toward taking control of the view context — is to set a query generation. In Persistence.swift, add this to init(inMemory:)
before the NotificationCenter
publisher:
if !inMemory {
do {
try viewContext.setQueryGenerationFrom(.current)
} catch {
// log any errors
}
}
You are pinning the view context to the most recent transaction in the persistent store with the call to setQueryGenerationFrom(_:)
. However, because setting query generation is only compatible with an SQLite store, you do so only if inMemory
is false
.
Step 2: Saving the History Token
Your history request uses a date to limit the results, but there's a better way.
An NSPersistentHistoryToken
is an opaque object that marks a place in the persistent store's transaction history. Each transaction object returned from a history request has a token. You're able to store it so you know where to start when you query persistent history.
You'll need a property in which to store the token for use while the app is running, a method to save the token as a file on disk and a method to load it from the saved file.
Add the following property to PersistenceController
just after historyRequestQueue
:
private var lastHistoryToken: NSPersistentHistoryToken?
That'll store the token in memory and, of course, you need a place to store it on disk. Next, add this property:
private lazy var tokenFileURL: URL = {
let url = NSPersistentContainer.defaultDirectoryURL()
.appendingPathComponent("FireballWatch", isDirectory: true)
do {
try FileManager.default
.createDirectory(
at: url,
withIntermediateDirectories: true,
attributes: nil)
} catch {
// log any errors
}
return url.appendingPathComponent("token.data", isDirectory: false)
}()
tokenFileURL
will attempt to create the storage directory the first time you access the property.
Next, add a method to save the history token as a file to disk:
private func storeHistoryToken(_ token: NSPersistentHistoryToken) {
do {
let data = try NSKeyedArchiver
.archivedData(withRootObject: token, requiringSecureCoding: true)
try data.write(to: tokenFileURL)
lastHistoryToken = token
} catch {
// log any errors
}
}
This method archives the token data to a file on disk and also updates lastHistoryToken
.
Return to processRemoteStoreChange(_:)
and find the following code:
let request = NSPersistentHistoryChangeRequest
.fetchHistory(after: .distantPast)
And replace it with this:
let request = NSPersistentHistoryChangeRequest
.fetchHistory(after: self.lastHistoryToken)
This simply changes from requesting the whole history to requesting the history since the last time the token was updated.
Next, you can grab the history token from the last transaction in your returned transaction array and store it. Under the print()
statement, add:
if let newToken = transactions.last?.token {
self.storeHistoryToken(newToken)
}
Build and run, watch the Xcode console, and tap the refresh button. The first time you should see all the transactions from the beginning. The second time you should see far fewer and perhaps none. Now that you've downloaded all the fireballs and stored the last transaction history token, there are probably no newer transactions.
Unless there's a new fireball sighting!
Step 3: Loading the History Token
When your app starts, you'll also want it to load the last saved history token if it exists, so add this method to PersistenceController
:
private func loadHistoryToken() {
do {
let tokenData = try Data(contentsOf: tokenFileURL)
lastHistoryToken = try NSKeyedUnarchiver
.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
} catch {
// log any errors
}
}
This method unarchives the token data on disk, if it exists, and sets the lastHistoryToken
property.
Call this method by adding it to the end of init(inMemory:)
:
loadHistoryToken()
Build and run and watch the console again. There should be no new transactions. In this way your app will be ready to query the history log, right off the bat!
Step 4: Setting a Transaction Author
You can refine your history processing even further. Every Core Data managed object context can set a transaction author. The transaction author is stored in the history and becomes a way to identify the source of each change. It's a way you can tell changes made by your user directly from changes made by background import processes.
First, at the top of PersistenceController
, add the following static properties:
private static let authorName = "FireballWatch"
private static let remoteDataImportAuthorName = "Fireball Data Import"
These are the two static strings that you'll use as author names.
Next, add the following to line to init(inMemory:)
, right below the call to set viewContext.automaticallyMergesChangesFromParent
:
viewContext.transactionAuthor = PersistenceController.authorName
This sets the transaction author of the view context using the static property you just created.
Next, scroll down to batchInsertFireballs(_:)
and, within the closure you pass to performBackgroundTask(_:)
, add this line at the beginning:
context.transactionAuthor = PersistenceController.remoteDataImportAuthorName
This sets the transaction author of the background context used for importing data to the other static property. So now the history that's recorded from changes to your contexts will have an identifiable source, and importantly, different from the transaction author for UI-updates like deleting by swiping a row.
Step 5: Creating a History Request Predicate
To filter out any transactions caused by the user, you'll need to add a fetch request with a predicate.
Find processRemoteStoreChange(_:)
and add the following right before do
:
if let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest {
historyFetchRequest.predicate =
NSPredicate(format: "%K != %@", "author", PersistenceController.authorName)
request.fetchRequest = historyFetchRequest
}
First, you create an NSFetchRequest
using the class property NSPersistentHistoryTransaction.fetchRequest
and set its predicate. The predicate test will return true
if the transaction author
is anything other than the string you created to identify the transactions made by the user. Then, you set the fetchRequest
property of the NSPersistentHistoryChangeRequest
with this predicated fetch request.
Build and run, and watch the console. You'll see the result of all this work. Delete a fireball and you'll see no transactions printed to the console because you're filtering out the transactions generated by the user directly. However, if you then tap the refresh button, you'll see a new transaction appear, because that's a new record added by the batch import. Success!
Phew! That was a long stretch — how are you doing? In these trying times, it's always good to remember your app's core mission: to save humanity from alien invasion. It's all worth it!