TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: SwiftData Model with an array of another Model inside - problem

Forums > Swift

So, I ran into a problem implementing SwiftData into my project. I've created a simpler one, with the same main idea, with the same problem.

I have a model, with an array of another model inside:

@Model
class Person {
    let name: String
    var mainItem: Item
    var items: [Item]

    init(name: String, mainItem: Item, items: [Item] = []) {
        self.name = name
        self.mainItem = mainItem
        self.items = items
    }
}

@Model
class Item {
    let name: String
    let size: Int

    init(name: String, size: Int) {
        self.name = name
        self.size = size
    }
}

The Person model is put into container:

@main
struct SwiftDataTestApp: App {
var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Person.self)
    }
}

And then for the problem itself:

struct ContentView: View {
  @Query var persons: [Person]
  @Environment(\.modelContext) var modelContext

  var body: some View { 
  // some View here listing persons and their items
  // also some Button running addItems()
  }

  func addItems() {
        if let person = persons.first(where: {$0.name == "Thomas"}) {
            person.mainItem = Item(name: "Backpack", size: 100)
            person.items = [
                Item(name: "Laptop", size: 10),
                Item(name: "Notebook", size: 10),
                Item(name: "Pencil", size: 5)
            ]
        }
    }
 }

What I want, is when the function is run, for it to find a person named Thomas, swap his main item to a Backpack, and put 3 items "in". I dont care if he had any items before (inside the array), i want him to have just those 3 new items.

When he starts with an empty array, it works just fine, I give him the items, he has them, the UI shows it, I restart the app, he still has them. All good.

But then I run the function again - i dont care what items you have before, you now (again) have a Backpack, and [Laptop, Notebook, Pencil]. All good, the UI based on persons array still shows Thomas having 3 items. BUT, when i restart the app, Thomas shows now that he has more than 3 items, ex. [Laptop, Notebook, Notebook, Pencil]. So the model accepted the 3 new items, but didnt dispose all of the previous values. If i run the function multiple times in a row, fast, after each run the UI shows Thomas having 3 items, and after restarting the app somehow he has like 10 or so items.

If I change the array inside Person model to be [String] for example, and the function now says:

person.items = ["Laptop", "Notebook", "Pencil"]

It works as it suppose to, clearing array of any previous value, I can run the func 100 times, restart the app and Thomas has 3 items i gave him. If the array holds any custom objects, it goes bad mixing old value with the new, resulting in unpredictable array.

Am I missing something here? Is this SwiftData bug?

2      

Hi! I have a question to you. If you don't use any relationship between those parts of your data, why do you mark both as @Model?

If there is no relationship required use struct for that.

@Model
class Person {
    let name: String
    var mainItem: Item
    var items: [Item]

    init(name: String, mainItem: Item, items: [Item] = []) {
        self.name = name
        self.mainItem = mainItem
        self.items = items
    }
}

struct Item: Codable, Hashable {
    let name: String
    let size: Int

    init(name: String, size: Int) {
        self.name = name
        self.size = size
    }
}

Otherwise you creating models many times that just sitting in the db.

3      

I remember adding @Model to the Item struct/class because i got errors while creating Person class without it. I removed @Model, changed it to struct and reinstalled the app and it seems to be the fix to my problem. Thank you @ygeras!

2      

hi,

i am speculating: if a model of type A holds on to an array of models of type B, then SwiftData infers that as a relationship and infers that there is an inverse (even though you did not ask for either).

i am also speculating: to directly set person.items = [ ... ] is probably not a good thing, since SwiftData is hiding what is really Core Data underneath. did it really remove the previous relationships and set up new ones? or was it necessary to explicity remove each of the existing A -- B relationships by removing items from person.items and then explicitly append new items to person.items?

i don't know ... and i'd agree with Yuri that some of those previous B models are probably now hanging around the database as orphans associated with no A.

as for an array of Strings, well those are not @Model objects, and SwifData seems able to handle it. but i'd guess that an array of and element type is probably only supported for types that are supported already in Core Data as transformable ... String types are one such type, as are UUIDs and arrays of such.

hope that helps,

DMG

3      

Seems like @delawaremathguy's speculations to be exactly what is going on behind the scenes. I just recreated the similar behavior and watched what is going on in the database.

Basically, Paul mentions about implicit relashionships in SwiftData by Example as well as other sources, mention the same, advising explicitly declare them to avoid confusion.

if a model of type A holds on to an array of models of type B, then SwiftData infers that as a relationship and infers that there is an inverse (even though you did not ask for either).

So to my understanding, the behavior in this particular case as follows. You create those implicit relationships and they are in the db. Once you replace again items in backpack.

person.items = [
                Item(name: "Laptop", size: 10),
                Item(name: "Notebook", size: 10),
                Item(name: "Pencil", size: 5)
            ]

seems like your view just retrieve that part of data from the db, but basically it still keeps previous as well, and once you reload your app, all relationships are retrieved again that is why you have them all back.

in red as can be seen from the pic, we will have primary key for person, meaning the relationships exist. So once the view accesses items property all data is back on the screen.

3      

Hacking with Swift is sponsored by Blaze.

SPONSORED Still waiting on your CI build? Speed it up ~3x with Blaze - change one line, pay less, keep your existing GitHub workflows. First 25 HWS readers to use code HACKING at checkout get 50% off the first year. Try it now for free!

Reserve your spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.