SwiftUI - List multi selection move / reorder (works on Mac but not on iOS)

123 Views Asked by At

How can I enable multi-select and then move / reorder selected items in a List with ForEach (in SwiftUI)?

I tried the following code. On Mac it works fine - it allows me to select multiple items, and then drag them all together to another place in the list. On iOS it allows me to move individual items with drag-and-drop, and allows me to enter Edit mode to select multiple items but when I try to move the selected items with drag-and-drop, they can be dragged but they can't be dropped elsewhere in the list:

struct ExampleListView: View {
    @State var items = ["Dave", "Tom", "Jeremy", "Luke", "Phil"]
    
    @State var selectedItems: Set<String> = .init()
    
    var body: some View {
        NavigationView {
            List(selection: $selectedItems) {
                ForEach(items, id: \.self) { item in
                    Text(item).tag(item)
                }.onMove(perform: move)
            }
            .navigationBarItems(trailing: EditButton())
        }
    }
    
    func move(from source: IndexSet, to destination: Int) {
        items.move(fromOffsets: source, toOffset: destination)
    }
}

Demo

Edit - This code (which uses List only and doesn't use ForEach) has the same issue:

struct ExampleListView: View {
    @State var items = ["Dave", "Tom", "Jeremy", "Luke", "Phil"]
    
    @State var selectedItems: Set<String> = .init()
    
    var body: some View {
        NavigationView {
            List($items, id: \.self, editActions: .all, selection: $selectedItems) { $item in
                Text(item).tag(item)
            }
            .navigationBarItems(trailing: EditButton())
        }
    }
    
    func move(from source: IndexSet, to destination: Int) {
        items.move(fromOffsets: source, toOffset: destination)
    }
}
1

There are 1 best solutions below

1
Balasubramanian On

One way to achieve this is to handle the drag-and-drop interactions manually. Here's how you can modify your code to enable multi-item drag-and-drop on both macOS and iOS

import SwiftUI

struct ExampleListView: View {
 @State var items = ["Dave", "Tom", "Jeremy", "Luke", "Phil"]

 @State var selectedItems: Set<String> = .init()
 @State var draggedItems: [String] = []

 var body: some View {
    NavigationView {
        List {
            ForEach(items, id: \.self) { item in
                Text(item)
                    .gesture(
                        DragGesture()
                            .onChanged { value in
                                if !self.selectedItems.contains(item) {
                                    self.selectedItems = [item]
                                }
                                self.draggedItems = Array(self.selectedItems)
                            }
                    )
                    .onDrag {
                        NSItemProvider(object: NSString(string: item))
                    }
                    .onDrop(of: [UTType.text], delegate: MyDropDelegate(item: item, listItems: $items, selectedItems: $selectedItems, draggedItems: $draggedItems))
            }
        }
        .navigationBarItems(trailing: EditButton())
        }
    }
}

struct MyDropDelegate: DropDelegate {
let item: String
@Binding var listItems: [String]
@Binding var selectedItems: Set<String>
@Binding var draggedItems: [String]

func performDrop(info: DropInfo) -> Bool {
    let destinationIndex = self.listItems.firstIndex(of: item) ?? 0
    var startIndex = 0
    
    for i in 0..<listItems.count {
        if draggedItems.contains(listItems[i]) {
            let item = listItems.remove(at: i)
            listItems.insert(item, at: destinationIndex + startIndex)
            startIndex += 1
        }
    }
    
    selectedItems = []
    draggedItems = []
    
    return true
  }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ExampleListView()
    }
}