I would like to use .onDrag
and .onDrop
to handle reordering of items in a LazyVGrid
(.draggeble should not be used because it automatically adds a + icon).
While I found several tutorials dealing with this topic, they all share a common problem: They use a full screen Grid and thus there is no place where dropping is not possible. Thus performDrop
or dropExited
is always called and can be used to detect when a drop operation has ended or was canceled.
In my example code the grid occupies only half of the screen. Thus when dropping an item in the yellow, bottom "No Drop" area, this action is not detected and thus the draggedItem
is not reset and stays invisible.
How can this be solved?
I really do not understand how the Drag/Drop API can lack such an important feature as onDropCanceled
or onDropEnded
. Is it really the best option to use some hacky Bindings to keep track of the active item manually?
struct DragTestView: View {
class ViewModel: ObservableObject {
@Published var items: [String] = Array(0..<9).map { "\($0)" }
}
@StateObject var viewModel = ViewModel()
@State private var draggedItem: String?
@State private var dragEndTimestamp = Date.distantPast
var body: some View {
VStack(spacing: 0) {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(minimum: 100)), count: 3)) {
ForEach(viewModel.items, id: \.self) { item in
Text("\(item)")
.frame(maxWidth: .infinity, maxHeight: 100)
.padding()
.background(Color.blue)
.opacity(draggedItem == item ? 0.001 : 1)
.onDrag{
print("Drag \(item)")
guard Date().timeIntervalSince(dragEndTimestamp) >= 2 else {
print("Too fast")
return NSItemProvider()
}
draggedItem = item
return NSItemProvider(object: item as NSString)
} preview: {
Text("\(item)")
.padding()
.background(Color.red)
}
.onDrop(of: [.text], delegate: ItemDropDelegate(item: item, draggedItem: $draggedItem, model: viewModel))
}
}
.padding()
.frame(maxHeight: .infinity)
.background(.green)
Text("No drop here")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.yellow)
}
.onChange(of: draggedItem) {
dragEndTimestamp = Date()
print("Changed dragged item to: \(draggedItem ?? "nil")")
}
}
struct ItemDropDelegate: DropDelegate {
let item: String
@Binding var draggedItem: String?
var model: DragTestView.ViewModel
func performDrop(info: DropInfo) -> Bool {
draggedItem = nil
return true
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func dropEntered(info: DropInfo) {
guard let draggedItem = draggedItem, draggedItem != item,
let from = model.items.firstIndex(of: draggedItem),
let to = model.items.firstIndex(of: item) else {return}
model.items.move(fromOffsets: IndexSet(integer: from), toOffset: to > from ? to + 1 : to)
}
}
}