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
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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.

Fewer transactions per notification after enabling history

Unless there's a new fireball sighting!

Yes! More fireballs!

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!

Fireball list

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.

Note: It's important to always have a context author if you're recording transaction history.

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!

Only background updates in update notifications

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!

The invasion isn't working!