简体   繁体   中英

SwiftUI Drag and Drop reorder - detect object release

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:

  1. When the item was onDrag and then released in place without moving.

  2. 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
}

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:

    // for demo simplicity, a convenient init can be created instead
    class MYItemProvider: NSItemProvider {
        var didEnd: (() -> Void)?
        deinit {
            didEnd?()     // << here !!
        }
    }

// ...

    let provider = MYItemProvider(contentsOf: URL(string: "\(item.id)"))!
    provider.didEnd = {
        DispatchQueue.main.async {
            draggingItem = nil      // << here !!
        }
    }

Complete test module is here

For iOS you can add .onDrop to the fullscreen view and catch performDrop 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

Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { timer in
    if (NSApplication.shared.currentEvent?.type == .leftMouseUp) {
       //perform your endDrop action                
       timer.invalidate()
    }
}

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM