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 2 of 4 of this article. Click here to view the first page.

Batch Inserting Fireballs

So that's the request creation done. Now, how do you use it? Add the following method to PersistenceController:

private func batchInsertFireballs(_ fireballs: [FireballData]) {
  // 1
  guard !fireballs.isEmpty else { return }

  // 2
  container.performBackgroundTask { context in
    // 3
    let batchInsert = self.newBatchInsertRequest(with: fireballs)
    do {
      try context.execute(batchInsert)
    } catch {
      // log any errors
    }
  }
}

Here's what that does:

  1. You first check that there's actually work to do, making sure the array is not empty.
  2. Then ask the PersistentContainer to execute a background task using performBackgroundTask(_:).
  3. Create the batch insert request and then execute it, catching any errors it might throw. The batch request inserts all of your data into the persistent store in a single transaction. Because your Core Data model has a unique constraint defined, it'll only create new records if they do not exist and update existing records if required.

One final change: Go to fetchFireballs() and, instead of calling self?.importFetchedFireballs($0), change it to:

self?.batchInsertFireballs($0)

You may also comment or delete importFetchedFireballs(_:), because it's no longer needed.

Note: If you were wondering, batch insert requests cannot set Core Data entity relationships, but they'll leave existing relationships untouched. See Making Apps with Core Data from WWDC2019 for more information.

All that's left to do is build and run!

Fireball list and details screens

But you may notice that something is wrong. If you delete a fireball and then tap the refresh button again, the list doesn't update. That's because the batch insert request inserts data into the persistent store, but the view context is not updated, so it has no idea that anything has changed. You can confirm this by restarting the app, after which you'll see that all the new data now appears in the list.

We'll need a new strategy

Previously, you were creating objects in the background queue context and saving the context, which pushed the changes to the persistent store coordinator. It was automatically updated from the persistent store coordinator after saving the background context because you have automaticallyMergeChangesFromParent set to true on the view context.

Part of the efficiency of persistent store requests is that they operate directly on the persistent store and avoid loading data into memory, or generating context save notifications. So while the app is running, you'll need a new strategy for updating the view context.

Enabling Notifications

Of course, updating the store in the background is not an uncommon situation. For example, you might have an app extension which updates the persistent store, or your app supports iCloud and your app's store updates from another device's changes. Happily, iOS offers a notification — NSPersistentStoreRemoteChange — which is delivered whenever a store update occurs.

Open Persistence.swift again and jump to init(inMemory:). Just before the line that calls loadPersistentStores(completionHandler:) on PersistentContainer, add this line:

persistentStoreDescription?.setOption(
  true as NSNumber,
  forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

Adding that one line causes your store to generate notifications whenever it updates.

Now, you need to use this notification somehow. First, add an empty method to PersistenceController, that'll be a placeholder for all of your update processing logic:

func processRemoteStoreChange(_ notification: Notification) {
  print(notification)
}

Your placeholder method simply prints the notification to the Xcode console.

Next, subscribe to the notification using the NotificationCenter publisher by adding this to the end of init(inMemory:):

NotificationCenter.default
  .publisher(for: .NSPersistentStoreRemoteChange)
  .sink {
    self.processRemoteStoreChange($0)
  }
  .store(in: &subscriptions)

Whenever your app receives the notification, it will call your new processRemoteStoreChange(_:).

Build and run, and you'll see a notification arrive in the Xcode console for each update. Try refreshing the fireball list, adding groups, deleting fireballs and so on. All updates to the store will generate a notification.

Xcode console showing notifications

So how does this notification help you? If you want to keep it simple, you can simply refresh the view context whenever you receive the notification. But there's a smarter, and much more efficient, way. And this is where you dive into persistent history tracking.

Enabling Persistent History Tracking

If you enable persistent history tracking, Core Data retains the transaction history of everything going on in your persistent store. This enables you to query the history to see exactly what objects were updated or created and merge only those changes into your view context.

To enable persistent history tracking, add this line in init(inMemory:) just before the line that calls loadPersistentStores(completionHandler:) on PersistentContainer:

persistentStoreDescription?.setOption(
  true as NSNumber, 
  forKey: NSPersistentHistoryTrackingKey)

That's it! Now, the app will save the transaction history of every change to your persistent store and you can query that history with a fetch request.

Making a History Request

When your app receives the store's remote change notification, it can now query the store's history to discover what's changed. Because store updates can come from multiple sources, you'll want to use a serial queue to perform the work. That way, you'll avoid conflicts or race conditions processing multiple sets of changes if they happen simultaneously.

Add the queue property to your class just before init(inMemory:):

private lazy var historyRequestQueue = DispatchQueue(label: "history")

Now, you can return to processRemoteStoreChange(_:), remove the print() statement and add the following code that'll perform a history request:

// 1
historyRequestQueue.async {
  // 2
  let backgroundContext = self.container.newBackgroundContext()
  backgroundContext.performAndWait {
    // 3
    let request = NSPersistentHistoryChangeRequest
      .fetchHistory(after: .distantPast)

    do {
      // 4
      let result = try backgroundContext.execute(request) as? 
        NSPersistentHistoryResult
      guard 
        let transactions = result?.result as? [NSPersistentHistoryTransaction],
        !transactions.isEmpty 
      else {
        return
      }
       
      // 5
      print(transactions)
    } catch {
      // log any errors
    }
  }
}

Here what's going on in the code above:

  1. You run this code as a block on your history queue to handle each notification in a serial manner.
  2. To perform the work, you create a new background context and use performAndWait(_:) to run some code in that new context.
  3. You use NSPersistentHistoryChangeRequest.fetchHistory(after:) to return a NSPersistentHistoryChangeRequest, a subclass of NSPersistentStoreRequest, that you can execute to fetch history transaction data.
  4. You execute the request and coerce the results into an array of NSPersistentHistoryTransaction objects. The default result type of a history request is just such an array of objects. The objects also contain NSPersistentHistoryChange objects that are all the changes related to the transactions returned.
  5. This is where you'll process the changes. For now, you just print the returned transactions to the console.

Build and run and do the usual testing dance: Tap the refresh button, delete a few fireballs, refresh again and so on. You'll find the notifications arrive and an array of transaction objects prints to your Xcode console.

Transaction records in the Xcode console