Search code examples
arrayssortingswiftuiswiftdata

SwiftData sorting order of Query array elements


I have a simple test app with a behavior I just don't understand.

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [Item]

    var body: some View {
        NavigationView {
            List {
 //               ForEach(items) { item in
                ForEach(items.sorted(by: { $0.sortNr < $1.sortNr })) { item in
                    HStack{
                        Text("\(item.sortNr)")
                        Text("\(item.itemName)")
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        }
    }

    private func addItem() {
        withAnimation {
            let newItem1 = Item(itemName: "one", sortNr: 1)
            let newItem2 = Item(itemName: "two", sortNr: 2)
            let newItem3 = Item(itemName: "three", sortNr: 3)
            let newItem4 = Item(itemName: "four", sortNr: 4)
            modelContext.insert(newItem1)
            modelContext.insert(newItem2)
            modelContext.insert(newItem3)
            modelContext.insert(newItem4)
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                modelContext.delete(items[index])
            }
        }
    }
}

@Model
final class Item {
    let id = UUID()
    var itemName : String
    var sortNr : Int
    
    init(itemName: String, sortNr: Int) {
        self.itemName = itemName
        self.sortNr = sortNr
    }
}

When I use the commented ForEach loop without the sorted(by:..) the list of items is displayed in some random order. The deleting of items removes the respective selected item correctly. I understand that Query and modelContext.insert does not guarantee a specific order, if not specified.

When I use ForEach loop with the sorted(by..) as above the items are displayed in the sorted manner according to the sortNr. But when I swipe to delete one item, most often a "wrong" item is deleted. The offsets is the correct index of the entry I selected, but it seems like the [item] has different indices in the view body than in the deleteItems func.

What do I need to add/change in this cod to have the correct indices in the array. I can use the @Query(sort... as in my app the input to the List view is a dynamic parameter.


Solution

  • This really isn't a SwiftData issue at all. You have an array that you are putting into the ForEach in one order, and then you are changing that order within the ForEach, so the array has one order and the ForEach another.

    For example, you have these arrays:

    [2,3,1] and [2,3,1].sorted(). You actually have

    [2,3,1] and [1,2,3]. [2,3,1] is your original and [1,2,3] in in the ForEach

    So, when the user swipes to delete "1" in the view that the ForEach supplies, the OS says ok, we need to delete the first element from the array, so it goes to the original array and deletes the first element "2". The solution is simply to sort your original array, and then supply it to the ForEach so you are comparing apples to apples.

    struct ContentView: View {
        @Environment(\.modelContext) private var modelContext
        // Sort the actual query
        @Query(sort: \Item.sortNr) private var items: [Item]
    
        var body: some View {
            NavigationView {
                List {
                  ForEach(items) { item in
                    // The rest of your code here
    }               
    

    If you weren't allowing deletions, your code would be fine, but you are referencing a variable outside of what you have scoped in the ForEach that has a different set of rules.