I implemented a simple drag and drop for reordering items in a VStack/Scrollview
according to this Solution
I store the currently dragged item in a property called draggingItem
and set the opacity to 0 depending if it is nil or not.
When performDrop in the DropDelegate gets called I set draggingItem
back to nil to make the corresponding item visible again.
There are two scenarios where performDrop seems not to get called:
-
When the item was onDrag and then released in place without moving.
-
When the item does get released slightly offset the actual droparea.
This is causing that the item does not get visible again because draggingItem
does not get set to nil again.
Any Ideas for a better place for setting draggingItem
back to nil?
View:
struct ReorderingTestsView: View {
@State var draggingItem: BookItem?
@State var items: [BookItem] = [
BookItem(name: "Harry Potter"),
BookItem(name: "Lord of the Rings"),
BookItem(name: "War and Peace"),
BookItem(name: "Peter Pane")
]
var body: some View {
VStack{
ScrollView{
VStack(spacing: 10){
ForEach(items){ item in
VStack{
Text(item.name)
.padding(8)
.frame(maxWidth: .infinity)
}
.background(Color.gray)
.cornerRadius(8)
.opacity(item.id == draggingItem?.id ? 0.01 : 1) // <- HERE
.onDrag {
draggingItem = item
return NSItemProvider(contentsOf: URL(string: "(item.id)"))!
}
.onDrop(of: [.item], delegate: DropViewDelegate(currentItem: item, items: $items, draggingItem: $draggingItem))
}
}
.animation(.default, value: items)
}
}
.padding(.horizontal)
}
}
DropViewDelegate:
struct DropViewDelegate: DropDelegate {
var currentItem: BookItem
var items: Binding<[BookItem]>
var draggingItem: Binding<BookItem?>
func performDrop(info: DropInfo) -> Bool {
draggingItem.wrappedValue = nil // <- HERE
return true
}
func dropEntered(info: DropInfo) {
if currentItem.id != draggingItem.wrappedValue?.id {
let from = items.wrappedValue.firstIndex(of: draggingItem.wrappedValue!)!
let to = items.wrappedValue.firstIndex(of: currentItem)!
if items[to].id != draggingItem.wrappedValue?.id {
items.wrappedValue.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
}
TestItem:
struct BookItem: Identifiable, Equatable {
var id = UUID()
var name: String
}
4
Answers
I investigated a problem 1) and proposed solution in https://stackoverflow.com/a/72181964/12299030
The problem 2) can be solved with help of custom overridden item provider and action on
deinit
, `cause provider is destroyed when drag session is canceled.Tested with Xcode 13.4 / iOS 15.5
Main part:
Complete test module is here
For iOS you can add
.onDrop
to the fullscreen view and catchperformDrop
there.For macOS I could not find any solution with DropDelegate. For that reason you can use
NSApplication.shared.currentEvent?.type == .leftMouseUp
something like this
The undetected object-release, when the item is "onDrag", is caused by the dragged view’s opacity being set to 0. SwiftUI ignores interaction with views with opacity of 0.
When you drag over a another draggable item, dropEntered of that item is called and the reordering takes place. After the reordering, the drag is now over "itself". But since the opacity is set to 0, SwiftUI ignores the view, hence the drag no longer is over a drop-target. Due to that, on touch-up, the drop is canceled and performDrop is not being called.
If you still want the item to be "invisible", you can use a very low non-zero-value for opacity, like 0.001 and it will work. I found it looked quite nice, when using an opacity of 0.3 or 0.5.
I had same issues and here is my example with solution how to resolve it:
https://github.com/kvyat/DragAndDrop