SwiftData Saving Changes

How close is SwiftData to Core Data when deciding if it needs to save an object?

Apple hasn’t told us how much SwiftData shares implementation details with Core Data. We know they have compatibile persistent stores and a lot of the SwiftData API is familiar to Core Data.

I’ve shared some tips in the past to avoid unnecessary work when Core Data saves changes. Do those tips also apply to SwiftData?

AutoSaving By Default

One difference from Core Data is that SwiftData will save a changed context automatically. Anytime you insert a new object, change, or delete an object the context needs saving. The system autosaves the context in two situations:

  • On app lifecycle change. For example, if the app moves from the foreground to the background.
  • On expiry of a timer that starts when you change the context. I’ve not seen Apple comment on how long the timer is.

You can still manually save the context or even disable autosaving when creating the container:

var body: some View {
  ContentView()
  .modelContainer(for: Item.self, 
    isAutosaveEnabled: false)
}

Manually Saving A Context

It’s long been a recommendation not to save a Core Data managed object context unless it has changes. The SwiftData ModelContext class follows the Core Data approach and provides a hasChanges property we can test before saving the context:

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

Changing A Model Object

A SwiftData model object conforms to the PersistentModel protocol which provides two methods to tell you if the model has changes or has been deleted:

func hasChanges() -> Bool
func isDeleted() -> Bool

Note: A Core Data NSManagedObject also has properties to indicate if the object has been inserted or updated.

As with Core Data, SwiftData marks the object as changed if you call the setter on any of the properties of the object. That’s the case even if you don’t change the value of the property. For example, starting with an item that has no changes:

(lldb) p item.hasChanges()
(Bool) false

(lldb) p item.name
(String) "A"

If I set the name property without changing the value that still counts as a change:

item.name = "A"

The hasChanges() method now returns true:

(lldb) p item.hasChanges()
(Bool) true

If you want to avoid the unnecessary save operation check the value is changing before setting the property:

if item.name != name {
  item.name = name
}