Why does a SwiftData Model allow Array? How do you really store one?

Order is not preserved, meaning the Array that exists in memory is not likely the one that will get persisted and reloaded. That's not really conceptually an Array—that's halfway to a Set.

I'm not seeing a general purpose solution on the Apple Developer Forums yet, and I haven't thought of one myself yet. Have you?

What?! Please show a reproducible snippet.

let items = try! modelContext.fetch(FetchDescriptor<T>(sortBy: []))

This returns "array" items in arbitrary order, unless you specify a sort descriptor.

Doubly linked list if you need to remove arbitrary items?


Edit:

Wait, if you don't need O(1) lookup behaviour for your "arrays" (how else could you tolerate having a list?) maybe you just sort your items by a string ID as in vocabulary:

E.g. you have two items: A Z
When inserting third that would become: A M Z
If you have this list: A B ... Z
and need to insert an item between A and B then it becomes: A AM B Z

and so on.

And the key (ID) could be not a string but a more optimal data structure, e.g. an array of UInt8.

On the second thought, doubly linked list with fixed sized ID could be more efficient.

I've just discovered this issue today while seeing my SwiftUI lists reorder randomly, after attempting to insert a new item at the front of a model "stored" array.

I haven't seen any indication from the docs or API that this is intended behavior from SwiftData. Yet, this is noticeably absent as a known issue from the Xcode beta release notes. See here: SwiftData Known Issues

User-decided ordering is critical for my use-case, so I'll be trying out some workarounds until Apple (hopefully) fixes this before release.

In the meantime, I'll be sending a bug report. The more the merrier!

1 Like

Giving this idea a test:

SwiftData's array "vocabulary" storage
import SwiftUI
import SwiftData

@Model final class Card {
    var id: String?
    var key: Double

    init(id: String, key: Double) {
        self.id = id
        self.key = key
    }
}
 
struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query(sort: \.key, order: .forward) var cards: [Card]
    @State var selectedID: String?
    
    var body: some View {
        NavigationView {
            List {
                ForEach(cards) { card in
                    HStack {
                        Text(card.id ?? "")
                        Spacer()
                        Text(String(card.key)).font(.subheadline).foregroundColor(.secondary)
                    }.background(card.id == selectedID ? Color.red : Color.clear)
                }
            }
            .toolbar {
                ToolbarItemGroup(placement: .navigationBarTrailing) {
                    Button("+10") {
                        withAnimation {
                            for i in 0 ..< 10 {
                                let newCard = Card(
                                    id: String(i),
                                    key: Double(i)
                                )
                                modelContext.insert(newCard)
                            }
                        }
                    }
                    Button {
                        withAnimation {
                            if !cards.isEmpty {
                                // delete random card
                                let i = Int.random(in: 0 ..< cards.count)
                                modelContext.delete(cards[i])
                                selectedID = cards[i].id
                            }
                        }
                    } label: {
                        Image(systemName: "minus")
                    }
                    .disabled(cards.isEmpty)
                    
                    Button {
                        withAnimation {
                            // insert a new card in a random position
                            let count = cards.count
                            let i = count == 0 ? 0 : Int.random(in: 0 ..< count)
                            let prev = count == 0 ? 0 : cards[i].key
                            let next = i + 1 < count ? cards[i + 1].key : prev + 1
                            let newKey = (prev + next)/2
                            let newID = "\(newKey)"
                            selectedID = newID
                            
                            modelContext.insert(
                                Card(
                                    id: "\(newKey)",
                                    key: newKey
                                )
                            )
                        }
                    } label: {
                        Image(systemName: "plus")
                    }
                }
            }
        }
    }
}

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

I'm using a double key for simplicity just to test the idea. In real app that uses this strategy the key must be variable length (array of UInt8 will do).

On this one I am not sure how to proceed with the query to return items in the list order.

SwiftData, like SwiftUI, is an Apple platform framework, and questions that are just about using the framework should be asked over at the Apple developer forums.

I know, but it’s still off-topic here even if you didn’t get a response there.