A better solution to SwiftUI List Reordering on macOS


In SwiftUI there is a very useful .onMove modifier that we can use to provide easy drag drop list reordering.


List {
    ForEach(values) {
    ...
    }.onMove { indices, newOffset in
        values.move(
            fromOffsets: indices,
            toOffset: newOffset
        )
    }
}


However when we apply this it adds a drag gesture recogniser to each of the rows this means when we click on a text field within the row it takes a long time before the text field is focused.


In AppKit there is a method to filter clicks to a subregion of the row (such as a drag handle on the leading edge) but SwiftUI does not provide this customisability.


However we do have access to .moveDisabled(true) that can be used on rows to stop them being drag-able. What is great is that when this is applied these rows do not delay clicks, so focusing on text fields is immediate.


We can combine this with the .onHover modifier to to conditional only enable the dragging of rows while the user is hovering over a given region of the row, such as a drag handle.


struct ContentView: View {
    @State
    var values = ["a", "b", "c", "d", "e", "f", "g", "h"]
    
    @State
    var isHovering: Bool = false
    
    var body: some View {
        List {
            ForEach(0..<values.count, id: \.self) { index in
                HStack {
                    
                    Image(
                        systemName: "line.horizontal.3"
                    )
                    .onHover { hovering in
                        isHovering = hovering
                    }
                    
                    TextField("Value", text: $values[index])
                }.moveDisabled(!isHovering)
            }.onMove { indices, newOffset in
                self.values.move(
                    fromOffsets: indices,
                    toOffset: newOffset
                )
            }
        }
    }
}


This works since during the drag operation SwiftUI does not update the onHover call so even when the mouse is dragged away the item remains movable until the drag finishes.


With this applied focusing on TextFields, clicking on Buttons or NavigationLinks within the rows is immediate. Also dragging to re-order can only start while the user mouse hovers over the drag handle so you can even have other gesture based controls within your view (such a multi-line TextEditor) without the drag events being confused.