3

I'm using a DropDelegate to handle drag/drop in SwiftUI on iOS. I have a simplified sample app here:

https://gist.github.com/jhaungs/6ac2c3a34309b7115eef40925c27b1ab

The problem seems to be an interaction between the SwiftUI framework and my code. If you attempt to load the itemProvider from DropInfo on the dropEntered or dropUpdated call, the system never calls the loadData function on the model; but somehow the same code works fine when it's called from performDrop.

If you set a breakpoint or add a print statement in getItem, you can see that itemProviders is never empty; it has exactly one item provider of the correct type (utf8 text). But calling loadObject on it only invokes loadData, when getItem is called from performDrop. So there's some opaque internal state mediating the data transfer.

Any ideas why this is not consistent? It seems like a bug to me.

In dropEntered and dropUpdated, I want to know what I'm dragging so I can do stuff like highlight it, or animate it, or perhaps even forbid or cancel the drop, but there's no feedback.

I could be doing this all wrong, too. I've spent quite a bit of time on it, and this is the closest I've gotten it to working. It's not clear that one DropDelegate per item is correct or wise. The documentation is sorely lacking, especially working examples.

There was a suggestion on another StackOverflow question to assign the current item to a viewModel variable in the onDrag, but this creates race conditions where the variable is not consistently set or cleared.

SwiftUI | Using onDrag and onDrop to reorder Items within one single LazyGrid?

Jim Haungs
  • 184
  • 10
  • Did you figure this out? I'm having a similar issue. – Hunter Meyer Apr 28 '21 at 16:22
  • Soon after I wrote this, I gave up on drag/drop for this particular use case, and I did the operation a different way (popped up a list of move targets to choose from). But from what I've been able to gather, the actual drop could potentially be a very expensive operation, e.g., copying a group of files, so it does not actually do the drop until performDrop is called at the very end of the whole drag/drop cycle when the user commits to actually doing the drop. – Jim Haungs Apr 29 '21 at 17:31
  • I supposed this does make since. Hopefully as SwiftUI matures certain use-cases will be improved where this cannot occur (such as previewing a layout change). – Hunter Meyer Apr 29 '21 at 21:28

1 Answers1

2

It's a bit unclear to me what you're asking, but if all you need is drag and drop that works in SwiftUI, here's a suggestion based on the same example you linked:

import SwiftUI
import UniformTypeIdentifiers

struct GridData: Identifiable, Equatable {
    let id: String
}

//MARK: - Model

class Model: ObservableObject {
    @Published var data: [GridData]

    let columns = [
        GridItem(.flexible(minimum: 60, maximum: 60))
    ]

    init() {
        data = Array(repeating: GridData(id: "0"), count: 50)
        for i in 0..<data.count {
            data[i] = GridData(id: String("\(i)"))
        }
    }
}

//MARK: - Grid

struct DemoDragRelocateView: View {
    @StateObject private var model = Model()

    @State private var dragging: GridData? // I can't reset this when user drops view ins ame location as drag started
    @State private var changedView: Bool = false

    var body: some View {
        VStack {
            ScrollView(.vertical) {
               LazyVGrid(columns: model.columns, spacing: 5) {
                    ForEach(model.data) { d in
                        GridItemView(d: d)
                            .opacity(dragging?.id == d.id && changedView ? 0 : 1)
                            .onDrag {
                                self.dragging = d
                                changedView = false
                                return NSItemProvider(object: String(d.id) as NSString)
                            }
                            .onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging, changedView: $changedView))
                            
                    }
                }.animation(.default, value: model.data)
            }
        }
        .frame(maxWidth:.infinity, maxHeight: .infinity)
        .background(Color.gray.edgesIgnoringSafeArea(.all))
        .onDrop(of: [UTType.text], delegate: DropOutsideDelegate(current: $dragging, changedView: $changedView))
    }
}

struct DragRelocateDelegate: DropDelegate {
    let item: GridData
    @Binding var listData: [GridData]
    @Binding var current: GridData?
    @Binding var changedView: Bool
    
    func dropEntered(info: DropInfo) {
        
        if current == nil { current = item }
        
        changedView = true
        
        if item != current {
            let from = listData.firstIndex(of: current!)!
            let to = listData.firstIndex(of: item)!
            if listData[to].id != current!.id {
                listData.move(fromOffsets: IndexSet(integer: from),
                    toOffset: to > from ? to + 1 : to)
            }
        }
    }

    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }

    func performDrop(info: DropInfo) -> Bool {
        changedView = false
        self.current = nil
        return true
    }
    
}

struct DropOutsideDelegate: DropDelegate {
    @Binding var current: GridData?
    @Binding var changedView: Bool
        
    func dropEntered(info: DropInfo) {
        changedView = true
    }
    func performDrop(info: DropInfo) -> Bool {
        changedView = false
        current = nil
        return true
    }
}

//MARK: - GridItem

struct GridItemView: View {
    var d: GridData

    var body: some View {
        VStack {
            Text(String(d.id))
                .font(.headline)
                .foregroundColor(.white)
        }
        .frame(width: 60, height: 60)
        .background(Circle().fill(Color.green))
    }
}
```
lmunck
  • 505
  • 4
  • 12