Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

SwiftData duplicates values inside array on insert()

After copying and inserting instances I am getting strange duplicate values in arrays before saving.

My models:

@Model
class Car: Identifiable {
    @Attribute(.unique)
    var name: String
    var carData: CarData

    func copy() -> Car {
        Car(
            name: "temporaryNewName",
            carData: carData
        )
    }
}

@Model
class CarData: Identifiable {
    var id: UUID = UUID()
    var featuresA: [Feature]
    var featuresB: [Feature]

   func copy() -> CarData {
       CarData(
            id: UUID(),
            featuresA: featuresA,
            featuresB: featuresB
        )
    }
}

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

    @Attribute(.unique)
    var name: String

    @Relationship(
        deleteRule:.cascade,
        inverse: \CarData.featuresA
    )
    private(set) var carDatasA: [CarData]?

    @Relationship(
        deleteRule:.cascade,
        inverse: \CarData.featuresB
    )
    private(set) var carDatasB: [CarData]?
}

The Car instances are created and saved to SwiftData, after that in code:

        var fetchDescriptor = FetchDescriptor<Car>(
            predicate: #Predicate<Car> {
                car in
                car.name == name
            }
        )
        let cars = try! modelContext.fetch(
            fetchDescriptor
        )
        let car = cars.first!
        print("car featuresA:", car.featuresA.map{$0.name}) //prints ["green"] - expected
        let newCar = car.copy()
        newCar.name = "Another car"
        newcar.carData = car.carData.copy()
        print("newCar featuresA:", newCar.featuresA.map{$0.name}) //prints ["green"] - expected
        modelContext.insert(newCar)
        print("newCar featuresA:", newCar.featuresA.map{$0.name}) //prints ["green", "green"] - UNEXPECTED!
        
        /*some code planned here modifying newCar.featuresA, but they are wrong here causing issues, 
for example finding first expected green value and removing it will still keep the unexpected duplicate 
(unless iterating over all arrays to delete all unexpected duplicates - not optimal and sloooooow).*/

        try! modelContext.save()
        print("newCar featuresA:", newCar.featuresA.map{$0.name}) //prints ["green"] - self-auto-healed???

Tested on iOS 18.2 simulator and iOS 18.3.1 device. Minimum deployment target: iOS 17.4

The business logic is that new instances need to be created by copying and modifying previously created ones, but I would like to avoid saving before all instances are created, because saving after creating each instance separately takes too much time overall. (In real life scenario there are more than 10K objects with much more properties, updating just ~10 instances with saving takes around 1 minute on iPhone 16 Pro.)

Is this a bug, or how can I modify the code (without workarounds like deleting duplicate values) to not get duplicate values between insert() and save()?

As code posted before has some typos and I can't edit it anymore, created a simple Xcode project showcasing the problem: https://github.com/deni2s/SwiftDataTesting

As a workaround I can correct the arrays by inserting after modelContext.insert(newCar):

newCar.carData.featuresA = car.carData.featuresA
newCar.carData.featuresB = car.carData.featuresB

and do necessary modifications on correct arrays without doing modelContext.save() after each new object.

The code is less maintainable (need to remember now that by adding new CarData property with array value it needs to be copied here), but from performance perspective new objects with this workaround are created and saved in ~9 seconds compared to ~1 minute with previous "working" approach when modelContext.save() needed to be executed after each modelContext.insert(newCar).

But I am still looking for clean solution without these hacky workarounds (as real life project atm has more than 30 properties of array values).

Your copy methods are in my opinion flawed, you create a new instance of the main object but you re-use the relationship object instead of making a new copy of that as well, so called deep copying.

So in Car you should have

func copy() -> Car {
   Car(
       name: "temporaryNewName",
       carData: carData.copy() //<-- New instance
   )
}

And in CarData you need to do something similar but loop over the features array and copy each element.

Perhaps unrelated but why do you need both Car and CarData, can't they be merged and personally I prefer to use the @Relationship property wrapper for my relationship properties to make the intention clearer

Your copy methods are in my opinion flawed, you create a new instance of the main object but you re-use the relationship object instead of making a new copy of that as well, so called deep copying.

So in Car you should have

func copy() -> Car {
   Car(
       name: "temporaryNewName",
       carData: carData.copy() //<-- New instance
   )
}

The copy methods are such on purpose because sometimes I need copy of Car with carData referencing to the same CarData instance. For example it can be car for different market - like Opel in Europe and Vauxhall in UK - same carData (carData), different name.

If you notice I am doing deep copy in the sample code. Notice the newcar.carData = car.carData.copy()

Perhaps unrelated but why do you need both Car and CarData, can't they be merged and personally I prefer to use the @Relationship property wrapper for my relationship properties to make the intention clearer

About merging properties - this is just minimum mock project to showcase the issue. Real data objects are much more complicated and not related to the cars. There are 20 interconnected objects, equivalent to Car object has 6 properties and equivalent to CarData - like 40 properties, 30 of them being arrays. Maybe I could even use 2 models for showcasing the issue instead of 3...

And in CarData you need to do something similar but loop over the features array and copy each element.

I don't think that makes any sense, as Features are identifiable references. I don't need two instances of "green" feature, I need just one, to which two carData instances are referring.

I did minified the code, also pushed to https://github.com/deni2s/SwiftDataTesting, but will repost relevant parts bellow:

@Model
class Car: Identifiable {
    var id: UUID = UUID()
    var name: String
    var features: [Feature]

    init(id: UUID = UUID(), name: String, features: [Feature]) {
        self.id = id
        self.name = name
        self.features = features
    }

   func copy() -> Car {
       Car(
            id: UUID(),
            name: name,
            features: features

        )
    }
}

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

    @Attribute(.unique)
    var name: String

    @Relationship(
        deleteRule:.cascade,
        inverse: \Car.features
    )
    private(set) var cars: [Car]?

    init(id: Int, name: String, cars: [Car]? = nil) {
        self.id = id
        self.name = name
        self.cars = cars
    }
}
@ModelActor
actor BackgroundActor: Sendable {

    func prepareData() {
        try! modelContext.delete(model: Car.self)
        let car = Car(
            name: "BMW",
            features: [
                Feature(id: 1, name: "green"),
                Feature(id: 2, name: "slow")
            ]
        )
        modelContext.insert(car)
        try! modelContext.save()
    }

    func test(name: String = "BMW") {
        let fetchDescriptor = FetchDescriptor<Car>(
            predicate: #Predicate<Car> {
                car in
                true
            }
        )
        let cars = try! modelContext.fetch(
            fetchDescriptor
        )
        let car = cars.first!
        print("expected car features:\n\t", car.features.map{$0.name}) //prints ["slow", "green"] - expected
        let newCar = car.copy()
        print("expected newCar features:\n\t", newCar.features.map{$0.name}) //prints ["slow", "green"] - expected
        modelContext.insert(newCar)
//        newCar.features = car.features //this workaround helps!
        print("UNEXPECTED newCar features:\n\t", newCar.features.map{$0.name}) //prints ["slow", "green", "slow", "green"] - UNEXPECTED!

        /*some code planned here modifying newCar.features, but they are wrong here causing issues,
for example finding first expected green value and removing it will still keep the unexpected duplicate
(unless iterating over all arrays to delete all unexpected duplicates - not optimal and sloooooow).

        for i in 0..<newCar.features.count {
            if newCar.features[i].name == "green" {
                newCar.features.remove(at: i)
                print("this should remove the green feature")
                break //feature expected to be removed -> no need to continue iterations
            }
        }
         */

        try! modelContext.save()
        print("after save newCar features:\n\t", newCar.features.map{$0.name}) //prints ["slow", "green"] - self-auto-healed???
    }
}

So still wondering why after inserting (and before saving) car's features array gets duplicated values with duplicated ids. Is this a bug or some strange feature?

I didn't notice Feature was a many-to-many relationship, I thought the array was owned by the Car/CarData object, my bad.

There is really no point in making your models conform to Identifiable and have a custom id property since they already conform to this protocol (unless you have some specific reason to use your own identifier value). I have no idea if this could help with the duplicate Feature objects issue but maybe it could be worth trying by removing the id property and use the default one instead.

SwiftData duplicates values inside array on insert()
 
 
Q