Core Data Saving Changes

A couple of quick tips to avoid unnecessary save operations when working with Core Data.

Saving Core Data

The Core Data framework tracks changes you make to managed objects in an NSManagedObjectContext. When you insert new objects into the context, update, or delete objects, the changes are not saved until you save the context.

A lot can happen when you save a managed object context:

  • When you save a context Core Data commits the changes to the parent store. For the main queue’s viewContext saves happen to the persistent store coordinator.
  • If you save a child context, those changes are not saved to the persistent store until you also save the parent context.
  • If you make changes on a private, background context, you will most likely want to merge those changes into the main view context.
  • If you’re syncing with CloudKit the changes propagate to other devices.

Only Save A Context That Has Changes

To avoid unnecessary work, Apple recommends you check if a context has uncommitted changes before calling save. That leads to common extensions on NSManagedObjectContext. For example:

extension NSManagedObjectContext {
  func saveIfChanged() -> NSError? {
    guard hasChanges else { return nil }
    do {
      try save()
      return nil
    } catch {
      return error as NSError
    }
  }
}

The hasChanges boolean property of NSManagedObjectContext tells you if the context has uncommitted changes. An NSManagedObject has a few more options:

  • hasChanges: true if you have inserted, deleted or updated the object (a combination of the following properties).
  • isInserted: true when you insert the object into a context.
  • isUpdated: true when you change the object.
  • isDeleted: true when you delete the object (Core Data removes the object during the next save).

What’s A Change?

There’s one problem with relying on hasChanges to decide if you should save a context. Core Data marks a managed object as updated if you call a setter on any of the properties of the object. This happens even if you don’t change the value of the property.

We can see this if we examine the object in the debugger. My Log managed object has name and timestamp properties:

(lldb) po log
<Log: 0x600000fbf110> (entity: Log;
 id: 0x97128f951cffa77c
 <x-coredata://6C662A92-0A75-4F43-BF48-2A10B4DF4C97/Log/p1>;
 data: {
  name = Alpha;
  timestamp = "2022-05-23 13:43:57 +0000";
})

It starts out unchanged:

(lldb) po log.hasChanges
false

Then we set the name property, but without changing the value:

name = "Alpha"
...
log.name = name // nothing has changed

This is enough to mark isUpdated and hasChanges as true and will cause an unnecessary save of the context:

(lldb) po log.isUpdated
true

(lldb) po log.hasChanges
true

You can avoid an expensive save operation by checking if the value is changing before setting a property:

if log.name != name {
  log.name = name
}