I'm working on a SwiftUI project where I have a list of items displayed using the List view. I want to provide two functionalities: item replacement and item reordering within the list.
- Item Replacement: I want to be able to drag an item from the list and drop it onto another item to replace it. When dropped, the original item should be replaced with the dragged item.
- Item Reordering: I also want to enable the ability to reorder items within the list using drag and drop gestures. I have tried using the onDrag and onDrop modifiers individually, but I haven't been able to combine both functionalities successfully. When I use onMove for reordering, it overrides the onDrag behavior.
I'm seeking community opinions on how to combine these two functionalities effectively. How can I enable both item replacement and reordering within the list using the onDrag and onDrop modifiers simultaneously?
Any insights, suggestions, or code examples would be greatly appreciated.
import SwiftUI
struct ContentView: View {
@State private var items = ["Item 1", "Item 2", "Item 3", "Item 4"]
@State private var highlightedIndex: Int?
var body: some View {
List {
ForEach(items.indices, id: \.self) { index in
let item = items[index]
HStack {
Text(item)
.foregroundColor(highlightedIndex == index ? .red : .primary)
Spacer()
}
.onDrag {
NSItemProvider(object: NSString(string: "\(index)"))
}
.onDrop(of: [.text], delegate: MyDropDelegate(itemIndex: index, items: $items, highlightedIndex: $highlightedIndex))
}
.onMoveCommand(perform: moveItem)
}
}
func moveItem(_ direction: MoveCommandDirection) {
guard let sourceIndex = items.firstIndex(where: { $0 == items[highlightedIndex ?? 0] }) else {
return
}
var destinationIndex: Int
switch direction {
case .up:
destinationIndex = sourceIndex - 1
if destinationIndex < 0 {
destinationIndex = items.count - 1
}
case .down:
destinationIndex = sourceIndex + 1
if destinationIndex >= items.count {
destinationIndex = 0
}
@unknown default:
return
}
items.move(fromOffsets: IndexSet(integer: sourceIndex), toOffset: destinationIndex)
highlightedIndex = destinationIndex
}
}
struct MyDropDelegate: DropDelegate {
let itemIndex: Int
@Binding var items: [String]
@Binding var highlightedIndex: Int?
func performDrop(info: DropInfo) -> Bool {
let item = info.itemProviders(for: ["public.text"]).first
item?.loadObject(ofClass: NSString.self) { item, _ in
if let itemString = item as? NSString {
let intValue = itemString.intValue
let swiftInt = Int(intValue)
let sourceIndex = swiftInt
let droppedItem = items[sourceIndex]
if let highlightedIndex = highlightedIndex {
items[highlightedIndex] = droppedItem
}
}
}
return true
}
func validateDrop(info: DropInfo) -> Bool {
highlightedIndex = itemIndex
return info.hasItemsConforming(to: [.text])
}
func dropEntered(info: DropInfo) {
highlightedIndex = itemIndex
}
func dropExited(info: DropInfo) {
highlightedIndex = nil
}
}

