Bug in .onmove for SwiftData models

I modified the default Items example from xcode to include two models, a view with a query and a detail view. On this detail view I cannot properly move all items, only the top two, please see the attached video.

Especially strange is that some dragging does work and some does not. It changes with the number of sub-items on a todo. The models are SwiftData and it is have a one-to-many relationship. On the many relationship the problem exists.

How should the code be adjusted to make sure all items are draggable?

https://imgur.com/a/n1y7iXX

Below is all code necessary for the minimal example. I target iOS 17.5 and this shows on both preview, simulator and my iPhone.

Models

@Model
final class ToDo {
    var timestamp: Date
    var items: [Item]
    
    init(timestamp: Date) {
        self.timestamp = timestamp
        self.items = []
    }
}

@Model
final class Item {
    var timestamp: Date
    var done: Bool
    
    init(timestamp: Date, done: Bool) {
        self.timestamp = timestamp
        self.done = done
    }
}

ItemListView (Here is the problem!)

struct ItemListView: View {
    @Bindable var todo: ToDo
    
    var body: some View {
        List {
            ForEach($todo.items) { $item in
                Text(item.timestamp.description)

            }
            .onMove { indexSet, offset in
                todo.items.move(fromOffsets: indexSet, toOffset: offset)
            }
        }
        
    }
}

ContentView

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

    var body: some View {
        NavigationSplitView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        ItemListView(todo: item)
                    } label: {
                        Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        } detail: {
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = ToDo(timestamp: Date())
            modelContext.insert(newItem)
        }
    }

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

APP

@main
struct ListProjectApp: App {

    var sharedModelContainer: ModelContainer = {
        
        let schema = Schema([
            ToDo.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(ToDoContainer.create())
    }
}

actor ToDoContainer {
    
    @MainActor
    static func create() -> ModelContainer {
        let schema = Schema([
            ToDo.self,
            Item.self
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        let container = try! ModelContainer(for: schema, configurations: [modelConfiguration])
                
        
        let todo = ToDo(timestamp: Date())
        
        container.mainContext.insert(todo)

        let item1 = Item(timestamp: Date(), done: false)
        let item2 = Item(timestamp: Date(), done: false)
        let item3 = Item(timestamp: Date(), done: false)
        
        todo.items.append(item1)
        todo.items.append(item2)
        todo.items.append(item3)

        
        return container
    }
}
Answered by DTS Engineer in 788918022
            .onMove { indexSet, offset in
                todo.items.move(fromOffsets: indexSet, toOffset: offset)
            }

The above code doesn't work because SwiftData doesn't retain the order of a relationship.

To order the relationship shown in your code snippet, I'd probably consider the following:

  1. Use another query that provides the items of the specified todo for ItemListView.

  2. Add an order field for Item, and then use the field as a sort descriptor to order the result set for the Query in step #1.

  3. When the user changes the order in the item list, your app changes the order field of the relevant items. That change triggers a SwiftUI update, which then presents the items in the right order.

Best,
——
Ziqiao Chen
 Worldwide Developer Relation.

The issue might be reconciling the list after popping the view with the data changes. what is the exact error at runtime?

            .onMove { indexSet, offset in
                todo.items.move(fromOffsets: indexSet, toOffset: offset)
            }

The above code doesn't work because SwiftData doesn't retain the order of a relationship.

To order the relationship shown in your code snippet, I'd probably consider the following:

  1. Use another query that provides the items of the specified todo for ItemListView.

  2. Add an order field for Item, and then use the field as a sort descriptor to order the result set for the Query in step #1.

  3. When the user changes the order in the item list, your app changes the order field of the relevant items. That change triggers a SwiftUI update, which then presents the items in the right order.

Best,
——
Ziqiao Chen
 Worldwide Developer Relation.

Bug in .onmove for SwiftData models
 
 
Q