How to use the .onMove SwiftUI modifier to reorder SwiftData elements?

I am new to SwiftData and I'm trying to use the .onMove modifier to rearrange "ChecklistItems"

List {
            ForEach(items) { item in
                ChecklistItemsListRowView(item: item, checklist: checklist)
                    .onTapGesture {
                        item.completed.toggle()
                        save()
                    } // onTapGesture
            }
            .onDelete(perform: { indexes in
                for index in indexes {
                    modelContext.delete(checklist.items[index])
                } // *for*
            }) // onDelete
            
            .onMove { IndexSet, int in
                // TODO: Rearrange Elements
            } // onMove
            
        } // LIST

This is my ChecklistItem class:

@Model
final class ChecklistItem {
    @Attribute(.unique)
    var creationDate: Date
    
    var name: String
    var priority: Int
    var notes: String
    
    var completed: Bool
    
    var checklist: Checklist?
    
    init(creationDate: Date, name: String, priority: Int, notes: String, completed: Bool) {
        self.creationDate = creationDate
        self.name = name
        self.priority = priority
        self.notes = notes
        self.completed = completed
    }
}

extension ChecklistItem {
    @Transient
    static var preview = ChecklistItem(creationDate: Date(), name: "Item", priority: 2, notes: "This is a note.", completed: true)
}
Post not yet marked as solved Up vote post of gmanultimate Down vote post of gmanultimate
1.7k views

Replies

One way to do this is to have a property in the model that stores the relative order index.

Start by adding this property to the model class:

var orderIndex: Int

This will need to be added to the initialiser as well.

When querying for a list of items, you can sort by this property:

@Query(sort: \ChecklistItem.orderIndex) private var items: [ChecklistItem]


For the view's move action you can do this:

.onMove { source, destination in
    // Make a copy of the current list of items
    var updatedItems = items

    // Apply the move operation to the items
    updatedItems.move(fromOffsets: source, toOffset: destination)
    
    // Update each item's relative index order based on the new items
    // Can extract and reuse this part if the order of the items is changed elsewhere (like when deleting items)
    // The iteration could be done in reverse to reduce changes to the indices but isn't necessary
    for (index, item) in updatedItems.enumerated() {
        item.orderIndex = index
    }
}

That last part I would put in an extension like so:

extension [ChecklistItem] {
    func updateOrderIndices() {
        for (index, item) in enumerated() {
            item.orderIndex = index
        }
    }
}

and you can instead write this for better ease of use and reusability:

items.updateOrderIndices() // run after changes to the order of `items`


When you add a new item, you will need to provide a value for its orderIndex property. This would be resolved as the number of current items (as indexing starts at 0). So if there are 5 items (with indices 0, 1, 2, 3, 4), the new index would be 5.

Here's an example of how to do that:

// Fetch the number of all items that contribute to the relative index ordering
let descriptor = FetchDescriptor<ChecklistItem>(/* add filtering or sorting if needed */) 
let count = (try? modelContext.fetchCount(descriptor)) ?? 0

// Pass the next index to the new item
let newItem = ChecklistItem(..., orderIndex: count)
modelContext.insert(newItem)



This is only a basic solution and probably isn't the most optimised as any change, even to a single item, results in all the items being updated.

Hope it helps anyway!

  • Hi, thanks for code! But how I need to sort if I ned to sort model that inside another one? Here is code

    @Model final class Items{ @Attribute(.unique) var name: String

    init(name: String, orderIndex: Int) { self.name = name self.orderIndex = orderIndex }

    }

    @Model final class ItemsTitle{ @Attribute(.unique) var name: String

    init(name: String) { self.name = name } @Relationship(deleteRule: .cascade) var items: [Items] = []

    }

  • I'm not sure what you're asking. Would you not just use items.sorted { $0.orderIndex < $1.orderIndex } . If that's not the problem, then please add a new reply detailing the issue with proper code snippets.

  • I have 1 Model with array of other one inside (you can saw it in code). And to fetch array I need to take first (main) and then I can take array (2second model). I was not understanding how to do that...

    I used - itemTitles.items.sorted(by: { $0.orderIndex < $1.orderIndex }) - and it is working.

    Thanks Sorry for my ability to explain in English.

@doc_clemens There is a simple typo on how the extension is used instead of:

items.updateOrderIndices()

it should be performed on the copy ofitems

updatedItems.updateOrderIndices()

Attaching it to ForEach works for me.

.onMove { source, destination in
    var tempItems = items
    tempItems.move(fromOffsets: source, toOffset: destination)
    
    for (index, tempItem) in tempItems.enumerated() {
        if let item = items.filter({ $0.id == tempItem.id}).first {
            item.orderIndex = index
        }
    }
}

Model:

final class Item {
    var orderIndex: Int?

    init() {
        self.orderIndex = nil
    }
}