I'm trying to implement drag and drop in my VStack. I have implemented a DropDelegate like so:
private struct StandardDropDelegate: DropDelegate {
let destinationItem: StandardReorderedListItem
@Binding var sourceContent: [StandardReorderedListItem]
@Binding var draggedItem: StandardReorderedListItem?
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
print("dropped item: \(String(describing: draggedItem))\n===========================")
self.draggedItem = nil
return true
}
func dropEntered(info: DropInfo) {
guard let draggedItem,
let fromIndex = sourceContent.firstIndex(of: draggedItem),
let toIndex = sourceContent.firstIndex(of: destinationItem),
fromIndex != toIndex else { return }
withAnimation(.easeInOut) {
self.sourceContent.move(fromOffsets: IndexSet(integer: fromIndex),
toOffset: (toIndex > fromIndex ? (toIndex + 1) : toIndex))
}
}
}
And in the View itself:
public struct StandardReorderedListView: View {
@Binding private var content: [StandardReorderedListItem]
@State private var draggedItem: StandardReorderedListItem?
private let type: ReorderedListType = .standard
public init(_ content: Binding<[StandardReorderedListItem]>) {
self._content = content
}
@ViewBuilder
public var body: some View {
ScrollView(.vertical) {
VStack {
ForEach(content) { item in
ReorderedListItemView(state: .constant(.active), // Don't care
isDragging: Binding(get: { self.draggedItem?.id == item.id }, set: { _ in }),
label: item.label,
itemType: type,
icon: item.icon,
action: item.action)
.onDrag {
print("drag item: \(item)\n===========================")
self.draggedItem = item
return NSItemProvider(object: item.id.uuidString as NSString )
}
.onDrop(of: [.text],
delegate: StandardDropDelegate(destinationItem: item,
sourceContent: $content,
draggedItem: $draggedItem))
}
}
}
}
}
As you can see, I've added some console prints in both onDrag(_:)
and performDrop(info:)
. And the prints are like this:
drag item: StandardReorderedListItem(id: 3BDFE0B4-58DF-42C0-8FAB-109D3BFD191E, label: "Item 1", icon: Optional(SwiftUI.Image(provider: SwiftUI.ImageProviderBox<SwiftUI.Image.NamedImageProvider>)), action: Optional((Function)))
===========================
dropped item: Optional(StandardReorderedListItem(id: 3BDFE0B4-58DF-42C0-8FAB-109D3BFD191E, label: "Item 1", icon: Optional(SwiftUI.Image(provider: SwiftUI.ImageProviderBox<SwiftUI.Image.NamedImageProvider>)), action: Optional((Function))))
===========================
drag item: StandardReorderedListItem(id: 3BDFE0B4-58DF-42C0-8FAB-109D3BFD191E, label: "Item 1", icon: Optional(SwiftUI.Image(provider: SwiftUI.ImageProviderBox<SwiftUI.Image.NamedImageProvider>)), action: Optional((Function)))
===========================
Here you can see that the View is being dragged again right after the drop is performed. Even though it hasn't been touched at all right after I dragged and dropped the View. Can anyone help me to understand and fix what's going on here? Thank you.
PS: I can't use Transferable since I need to support at least iOS 15. So the new way to drag and drop won't be viable.
Based on the answer here: https://stackoverflow.com/a/72387161/4124849, we can trigger something when the ItemProvider is deinit-ed, which will happen when the dragging is finished. So, I made a derived class of NSItemProvider that will do just that like so:
class ReorderedItemProvider: NSItemProvider {
var didDeinit: (() -> Void)?
deinit {
didDeinit?()
}
}
So now I add it in the View:
public struct StandardReorderedListView: View {
@Binding private var content: [StandardReorderedListItem]
@State private var draggedItem: StandardReorderedListItem?
private let type: ReorderedListType = .standard
public init(_ content: Binding<[StandardReorderedListItem]>) {
self._content = content
}
@ViewBuilder
public var body: some View {
ScrollView(.vertical) {
VStack {
ForEach(content) { item in
ReorderedListItemView(state: .constant(.active), // Don't care
isDragging: Binding(get: { self.draggedItem?.id == item.id }, set: { _ in }),
label: item.label,
itemType: type,
icon: item.icon,
action: item.action)
.onDrag {
print("drag item: \(item)\n===========================")
self.draggedItem = item
let provider = ReorderedItemProvider(object: item.id.uuidString as NSString)
//-- Added this
provider.didDeinit = {
//-- Use Task to sleep for 100 ms just to be safe and to make sure we reset the draggedItem AFTER the last faulty `onDrag()`
Task {
try? await Task.sleep(nanoseconds: 100000000)
self.draggedItem = nil
}
}
return provider
}
.onDrop(of: [.text],
delegate: StandardDropDelegate(destinationItem: item,
sourceContent: $content,
draggedItem: $draggedItem))
}
}
}
}
}