Being Sendable with SwiftData

In the previous article, I showed how to setup a ModelActor which accessed SwiftData on a non-MainActor.

  1. Using ModelActor in SwiftData
  2. Being Sendable with SwiftData

We were left with an implementation that used a with pattern which allowed us to use a non-Sendable object within a closure. In this article I'm going to expand on that by resolving the issue with non-Sendable PersistentModel objects.

Why PersistentModels aren't Sendable

One of the revelations I received from my sessions at WWDC 2024 was that PersistentModel object are not Sendable. This means a PersistentModel cannot be passed from one actor to another. In other words, it's difficult to pass them from one function to another unless they are on the same actor so you must do whatever you want with them within that function. This also means - no holding references to a PersistentModel within an @Observable object.

Well then how can one hold a reference to a PersistentModel? This is where the persistentIdentifer come in.

The Power of PersistentIdentifier

In a great article from Xu Yang (aka FatBobMan) he goes over the different identifiers in Core Data and Swift Data. In our particular case, we'll be using the PersistentIdentifier from .persistentModelID:

extension ModelContext {
  func persistentModel<T>(withID objectID: PersistentIdentifier) throws -> T?
    where T: PersistentModel {
    if let registered: T = registeredModel(for: objectID) {
      return registered
    }
    if let notRegistered: T = model(for: objectID) as? T {
      return notRegistered
    }
  
    let fetchDescriptor = FetchDescriptor<T>(
      predicate: #Predicate { $0.persistentModelID == objectID },
      fetchLimit: 1`
`    )
  
    return try fetch(fetchDescriptor).first
  }
}

This will work great but we if we used a Phantom Type to store the PersistentModel type we are fetching.

Introducing the Model

In an article from Majid Jabrayilov he introduces the concept of the Phantom Type:

A phantom type is a generic type that is declared but never used inside a type where it is declared.

Typically we use a generics we are using it because a property stores a value of that type. However a Phantom Type simply uses it to note how the object is to be used. In our case we are going to use the generic type to denote what PersistentModel type we are fetching:

public struct Model<T: PersistentModel>: Sendable {
  public let persistentIdentifier: PersistentIdentifier

  public init(persistentIdentifier: PersistentIdentifier) {
    self.persistentIdentifier = persistentIdentifier
  }
}

extension Model {
  public init(_ model: T) {
    self.init(persistentIdentifier: model.persistentModelID)
  }
}

What's particularly great about this Model type, is that it's actually Sendable! We can then store this in SwiftUI or Observable object. Now let's go back and use this with our ModelContext:

extension ModelContext {
  func getOptional<T>(_ model: Model<T>) throws -> T?
    where T: PersistentModel {
      try self.persistentModel(withID: model.persistentIdentifier)
    }
  }
}

Putting all this together from our last article, we can use this in our Database type:

let itemModel : Model<Item>
database.withModelContext{ modelContext in 
  guard let item = modelContext.getOptional(itemModel) else {
    // shouldn't happen 😣
  }
  item.timestamp = Date()
  try modelContext.save()
}

I'm sure you're already thinking we should use this for some CRUD operations on our Database type. Well in the next article we'll explore how to add that to our API.