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

Strange issue with SwiftData

Forums > SwiftUI

Hi all, I'm fairly new to Swift and especially to SwiftData, so I hope someone here will be able to help me understand and hopefully solve an issue I have encountered. My data structure is inspired by the iExpense project (for anyone who is familiar with that project in the 100 days of SwiftUI course). That is, I have an object responsible for loading, saving and editing items, with a single property consisting of an array of those items, and a second model that defines the single item. This all works, except that every time I open the app or add a new item to the array, the order of the array changes in a seemingly random way, and even if I use the append() method, the new element sometimes is inserted at a different position than the end of the array. Here is a shortened version of my models and of the main view, where the loading happens: @Model class Wardrobes { @Relationship(inverse: \Wardrobe.wardrobes) var items: [Wardrobe]? = [] // * func addWardrobe(name: String, importingClothes: [Clothing], selectedWardrobes: [Wardrobe]) { // code to handle adding clothes and style information from other wardrobes to the new wardrobe, which I don't think is relevant for this issue self.items!.append(Wardrobe(id: UUID(), name: name, clothes: copiedClothes, styles: copiedStyles)) } // methods to edit and delete wardrobe objects, which I'll omit because they don't seem to trigger the issue }

@Model class Wardrobe: Identifiable, Equatable, Hashable { var wardrobes: Wardrobes? = nil // * var id: UUID = UUID() var name: String = "" var clothes: [Clothing] = [] var styles: [String: MatchInfo] = [:] init(id: UUID = UUID(), name: String = "", clothes: [Clothing] = [], styles: [String: MatchInfo] = [:]) { self.id = id self.name = name self.clothes = clothes self.styles = styles } // methods to add, edit, reorder and delete clothes, and conformances to the protocols listed. Adding a Clothing instance to the clothes array causes the issue when the clothes array was empty. }

  • Explicit and optional relationship is required because the app uses CloudKit integration.

struct ContentView: View { @Environment(.modelContext) var modelContext @State var wardrobes: Wardrobes? @State var currentWardrobe = Wardrobe(id: UUID(), name: "Default", clothes: [Clothing]()) var body: some View { NavigationStack { Group { // contains a list displaying the clothes of the current wardrobe, or an UnavailableContentView if currentWardrobe.clothes is empty. } .onAppear { if let wardrobes { print("ContentView appeared") if wardrobes.items!.isEmpty { print("Using default") wardrobes.items!.append(currentWardrobe) } else if currentWardrobe != wardrobes.items![settings.lastWardrobe] || currentWardrobe.clothes != wardrobes.items![settings.lastWardrobe].clothes || currentWardrobe.styles != wardrobes.items![settings.lastWardrobe].styles { print("Setting current wardrobe") currentWardrobe.id = wardrobes.items![settings.lastWardrobe].id currentWardrobe.name = wardrobes.items![settings.lastWardrobe].name currentWardrobe.clothes = wardrobes.items![settings.lastWardrobe].clothes currentWardrobe.styles = wardrobes.items![settings.lastWardrobe].styles } } } .onAppear { // load wardrobes let wardrobesRequest = FetchDescriptor<Wardrobes>() let wardrobesData = try? modelContext.fetch(wardrobesRequest) wardrobes = wardrobesData?.first ?? Wardrobes() print("Loaded wardrobes:") if let items = wardrobes?.items { for w in items { print("- (w.name)") // There is a dedicated view that displays the list of wardrobes and allows to add new ones, but the random reordering is already visible here, suggesting that said view is not the culprit } } } } }

I thought this could be some kind of SwiftData bug, so I tried to reproduce the issue in a simplified context: @Model class Items { @Relationship(inverse: \Item.items) var items: [Item]? = [] init(items: [Item] = []) { self.items = items } }

@Model class Item { var items: Items? = nil var name: String = "" init(name: String = "") { self.name = name } }

struct ContentView: View { @Environment(.modelContext) var modelContext @State var items: Items? var body: some View { NavigationStack { List { if items != nil { ForEach(items!.items!) { item in Text(item.name) } } } .navigationTitle("SwiftData Test") .toolbar { Button("Test") { items!.items!.append(Item(name: "Test")) } } .onAppear { print("Loading Items...") let request = FetchDescriptor<Items>() let data = try? modelContext.fetch(request) items = data?.first ?? Items() items!.items!.append(Item(name: "A")) items!.items!.append(Item(name: "B")) items!.items!.append(Item(name: "C")) } } } }

But this sample crashes (and I can't understand why)! More specifically, it crashes on the line "@Relationship(inverse: \Item.items) var items: [Item]? = []". The console prints "Loading Items..." and stops there: no fatal error or such, so I have no idea what's causing the crash. The call stack in the debug navigator shows the following: 0 specialized createModel #1 <τ_0_0><τ_1_0><τ_2_0><τ_30>(:) in createToManyRelationship #1 <τ_0_0><τ_1_0><τ_20>(:) in static _DefaultBackingData.getRelationship<τ_0_0>(key:managedObject:forType:) 5 PersistentModel.getValue<τ_0_0, τ_0_1>(forKey:) 6 Items.items.getter 7 Items.items.modify 8 closure #3 in closure #1 in ContentView.body.getter 9 ___lldb_unnamed_symbol162713 37 static SwiftDataTestApp.$main() 38 main 39 start

I encountered this crash in the main project as well, during my several attempts at resolving the issue. Somehow I managed to make it go away there, but the problem where the array changes randomly still persists. If someone more experienced than me would be so kind as to help me understand what's going on here, I would be grateful, because I'm really stumped! If you need more information just let me know, I'll be happy to provide it. Thanks for reading!

3      

hi Laura,

it's difficult to diagnose the code itself ... formatting would help: enter a new line with only three back-ticks, go to the next line, paste your code in, then end with a new line having only three back-ticks ... or you can use the </> button on the window's editing ribbon ... but i can at least answer what might be your primary question:

... except that every time I open the app or add a new item to the array, the order of the array changes in a seemingly random way, and even if I use the append() method, the new element sometimes is inserted at a different position than the end of the array.

even though SwiftData surfaces a one-to-many relationship in Swift as an array of, say, Item objects, it's actually a set of Items underneath in Core Data. that explains why you get different orderings in use.

if you want the Items to appear in a specific order, you should sort the array the way you want it to appear.

that said, there's lots more to consider as you get into SwiftData.

  1. i'd question whether you need to have a single, centralized object which manages a list of objects (you would be better off using a @Query(filter: sort: ...) var items: [Item] to pull out Items from SwiftData).
  2. i think your simplified example code may be failing because you never inserted any of the objects you created into the modelContext.
  3. and i'd especially recommend some name changes for readability (e.g., your simplified example has a curious use of items!.items!.append(Item(name: "A"))), although this may resolve itself if you resolve point number 1.

finally, here's what your sample code looks like when you strip out the centralized object that keeps a list of all the other objects:

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
                .modelContainer(for: [Item.self])
    }
}

@Model class Item {
    var name: String = ""

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

struct ContentView: View {

    @Environment(\.modelContext) var modelContext // note syntax on keypath with \.

    @Query(sort: \Item.name) var items: [Item]

    var body: some View {
        NavigationStack {
            List {
                ForEach(items) { item in
                    Text(item.name)
                }
            }
            .navigationTitle("SwiftData Test")
            .toolbar {
                Button("Insert a Test Item") {
                    modelContext.insert(Item(name: "1Test")) // will be sorted at the start of the list
                }
            }
            .onAppear {
                print("Loading Items...")
                modelContext.insert(Item(name: "A"))
                modelContext.insert(Item(name: "B"))
                modelContext.insert(Item(name: "C"))
            }
        }
    }
}

hope that helps,

DMG

3      

@delawaremathguy Apologies for the formatting issue: this was my first post here, plus I’m using VoiceOver and the message editor doesn’t seem to be fully accessible. Anyway, thanks a lot for the clarification about the underlying set used by Core Data: that certainly explains why I was getting that weird behaviour! Also, thanks for the tip about using a standard array with @Query instead of a custom object: I was starting to think that wasn’t the best approach. And as for the naming, you are probably right there too, even though that items.items thing was just a quick example I put together without much thinking (which in fact also caused me to completely forget that I have to insert new objects into the model context 😂). Thanks again for your help!@ò

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.