Append behaviour in SwiftData arrays

Hi there,

I'm getting my bearings with SwiftData, and noticed some strange behaviour with models containing other models via arrays. When I call regular old foo.bars.append(bar), where foo and bar are both SwiftData models, the result is that bar ends up in a random place in the bars array, and other elements in the bars array are potentially shuffled around.

Is there a way to override this behaviour? I'd like for append to behave like it would in regular circumstances.

Here's a relatively minimal example:

import Foundation
import SwiftData

@Model
final class Foo {
    @Relationship(deleteRule: .cascade) var bars: [Bar]
    
    init(bars: [Bar]) {
        self.bars = bars
    }
}

@Model
final class Bar {
    var date: Date
    
    init(date: Date = Date()) {
        self.date = date
    }
}
struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var foos: [Foo]

    var body: some View {
        NavigationStack {
            List {
                ForEach(foos) { foo in
                    ForEach(foo.bars) { bar in
                        Text(bar.date.formatted(date: .omitted, time: .standard))
                    }
                    Button {
                        withAnimation {
                            let bar = Bar()
                            modelContext.insert(bar)
                            foo.bars.append(bar)
                        }
                    } label: {
                        Text("add bar")
                    }
                }
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addFoo) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
            .navigationTitle("swift data test")
        }
    }

    private func addFoo() {
        withAnimation {
            let newFoo = Foo(bars: [])
            modelContext.insert(newFoo)
        }
    }
}
@main
struct SwiftDataTestApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([Foo.self])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}

I'm well and truly a beginner, so forgive the question if the answer is entirely obvious. Thanks for your help!

This probably isn't the right forum for your question since SwiftData isn't part of Swift directly. That said, the reason you're getting this behavior is that even though you've defined an Array the underlying Core Data implementation will use an NSSet since that's the default for to-many relationships in Core Data.

I don't know of any way to override this behavior to use a Core Data ordered relationship instead.

To solve this you could add an index or position property to your object and sort in-place through a computed property / function if you want to grab your related objects in an ordered fashion.

1 Like

Thank you, that makes sense! Is there a more suitable forum you'd suggest for Apple framework related questions?

withAnimation {
let bar = Bar()

                        foo.bars.append(bar)
                    }

That's the right way, use only append(bar)

1 Like

Thanks! So is it the case then that you only ever call modelContext.insert on root classes? And does the same rule apply to modelContext.delete?

Yes. To delete an object bar, the following syntax applies:

modelContext.delete(bar)