Upsert Issue with SwiftData's Attribute Schema Macro using the Unique Option

Upsert Issue with SwiftData's Attribute Schema Macro using the Unique Option

I recently encountered a puzzling crash while using SwiftData. The result? A cryptic Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) error message. The catalyst? Updating a record in the DB and then (simply) reading it. To be clear, this couldn’t be reproduced when the record was first created, only after it had been updated and then read.

My data model was very simple; it looked like so:

@Model
class Event {
    @Attribute(.unique)
    var id: Int

    var type: String
    var date: Date
        
    init(id: Int, type: String, date: Date) {
        self.id = id
        self.type = type
        self.date = date
    }
}

While experimenting, I found the origin of this crash to be calling save() on the ModelContext prior to reading the item. save(), however, is not neccessary since SwiftData can autosave an updated context. I had missed the note on that one 😅. I’m surprised it was detrimental, though.

Removing the .save() fixed the issue. Alternatively, making the entity field marked with the @Attribute(.unique) as optional solved the issue as well. In my scenario, I was able to just remove the .save() call on the ModelContext.

Some sample code demonstrating the problem can be found below:

import SwiftUI
import SwiftData

struct ContentView: View {
    @State var readValue = "<none>"
    var body: some View {
        VStack(spacing: 15) {
            Button(action: {
                readValue = TestBed.crashMe()
            }, label: {
                Text("Tap Twice to Crash")
            })
            
            Button(action: {
                readValue = TestBed.worksFine()
            }, label: {
                Text("Will Never Crash")
            })
            
            Text("Read Value: \(readValue)")
        }
    }

    enum TestBed {
        @MainActor static func crashMe() -> String {
            let result = insertItem()

            // This save operation creates a crash if the item is read
            do { try result.context.save() } catch { print(error) }  
            return result.item.name
        }
        
        @MainActor static func worksFine() -> String {
            let result = insertItem()
            return result.item.name
        }
        
        @MainActor static func insertItem() -> (item: Item, context: ModelContext) {
            let container = try! ModelContainer(for: Item.self)
            let context = container.mainContext
            let item = Item(name: "Hello World")
            context.insert(item)
            return (item: item, context: context)
        }
    }
}

@Model
final class Item {
    @Attribute(.unique) var name: String

    init(name: String) {
        self.name = name
    }
}



December 08, 2023


-->