SwiftData: Application crash when trying to save (and fetch) a Model with unique attribute a second time

The context is to create a model instance in SwitfData and return it. This model as a unique attribute (defined by @Attribute(.unique)). The application runs fine the first time but on the second run, it fails with EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP).

What the reason behind this crash?

Here is my sample application to duplicate the issue. The issue is reproducible on MacOS and iOS:

import SwiftUI
import SwiftData

@available(iOS 17, *)
@available(macOS 11, *)
@Model
public final class MyModel : CustomStringConvertible {
    @Attribute(.unique) var uuid: UUID

    init(uuid: UUID) throws {
        self.uuid = uuid
    }

    public var description: String {
        return self.uuid.uuidString
    }
}

@available(iOS 17, *)
@available(macOS 11, *)
@ModelActor
public actor LocalDatabaseService {
    public static let shared = LocalDatabaseService()

    let schema = Schema([MyModel.self])

    public init() {
        self.modelContainer = try! ModelContainer(for: self.schema)
        let context = ModelContext(modelContainer)
        self.modelExecutor = DefaultSerialModelExecutor(modelContext: context)
    }


    public func createMyModel(uuid: UUID) throws -> MyModel {
        let myModel = try MyModel(uuid: uuid)

        let modelContext = self.modelContext
        modelContext.insert(myModel)
        try modelContext.save()

        return myModel
    }
}


struct ContentView: View {
    var body: some View {
        Task {
            let id = UUID(uuidString: "9C66CA5B-D91C-480F-B02C-2D14EEB49902")!
            let myModel = try await LocalDatabaseService.shared.createMyModel(uuid: id)
            print("myModel:\(myModel)")
            print("DONE")
        }
        return VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    return ContentView()
}

Note: a workaround is to returned the fetched model instance return try self.getMyModel(uuid: uuid):

func getMyModel(uuid: UUID) throws -> MyModel {
    let fetchDescriptor = FetchDescriptor<MyModel>(predicate: #Predicate { $0.uuid == uuid })

    let serverList = try modelContext.fetch(fetchDescriptor)
    if !serverList.isEmpty {
        if let first = serverList.first {
            return first
        }
    }
    fatalError("Could not find MyModel with uuid \(uuid)")
}

... but it does not explain the crash.

I can't reproduce the crash you mentioned with Xcode 15.4 + the iOS simulator. If you can elaborate a bit how you crash the app, I may be able to take another look once I catch a chance.

Your code is probably just for experimental purpose, but since it is on the forums where many people get involved, I'd like to share several comments:

First, putting a task in a view body doesn't really make sense to me. Please consider using .task.

Secondly, returning a SwiftData model from an actor triggers a warning, if you set the Strict Concurrency Check build setting in Xcode to Complete, because a model isn't Sendable.

When your model is isolated with an actor, consider returning data that is Sendable. In your case, your code snippet just prints the model description, and so returning myModel.description works.

If you would use the model with the other model context, the mainContext of your model container, for example, consider returning the model's persistentModelID from your actor, and then use model(for:) to convert the persistent model ID to a model.

Finally, the way you use ModelActor seems uncommon to me. @ModelActor expands your actor with a public initializer: public init(modelContainer: SwiftData.ModelContainer). So I typically use ModelActor in the following way, with which I only provide the app's model container and don't need to take care the model context and executor.

@ModelActor
actor MyModelActor {
    func performIsolatedOperation() -> AnySendableType { ... }

func useMyModelActor() {
    Task {
        let myActor = MyModelActor(modelContainer: myModleContainer)
        let value = await myActor.performIsolatedOperation()
        print("\(value)")
    }
}

In your case, it seems like you are creating one singleton shared actor to provide database service for your app. If that is the case, you probably just use actor and not ModelActor so you can avoid the public initializer.

Best,
——
Ziqiao Chen
 Worldwide Developer Relation.

SwiftData: Application crash when trying to save (and fetch) a Model with unique attribute a second time
 
 
Q