Search code examples
swiftswiftdataswift-data-modelcontext

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.


Solution

  • One (undocumented?) way to deal with @Attribute(.unique) is to make them Optional. In my example, it will be @Attribute(.unique) var uuid: UUID?

    When you try to insert a model with the same unique attribute, this attribute will be nil after saving.

    I saw this trick here: https://forums.developer.apple.com/forums/thread/731483

    So my code could be:

    public func createMyModel(uuid: UUID) throws -> MyModel {
        let myModel = try MyModel(uuid: uuid)
    
        let modelContext = self.modelContext
        modelContext.insert(myModel)
        try modelContext.save()
    
        if myModel.uuid == nil {
            throw "Model with UUID \(uuid) already exist"
        }
    
        return myModel
    }