Drag item from Popover: No path forward

I am working on an iPadOS app that is fully SwiftUI (iOS14.0 beta1).

I am using a .popover(Menu()) to present a list of draggable elements to drag back into the main content view. But I cannot get this to work.

I set a .onDrag() on each draggable element.

If, in the onDrag(), I do not dismiss the popover ($isDisplayed = false), then the drag item cannot find anyplace to allow the drag to target. I think that this is from the popover being modal and blocking other views.

If I dismiss the popover, then a long-press to start the drag will also generally lose the item to drag. I can only maintain the drag with very specific actions being moving just as the long press turns the item into draggable element.

Any ideas on what I am doing wrong?
Post not yet marked as solved Up vote post of cwoloszynski Down vote post of cwoloszynski
726 views

Replies

Is there a solution for this? I'm having the same problem and there doesn't seem to be much information on solving this

I was also struggling with this issue - draggable views within a popover could not be dropped outside of it. The popover must be dismissed before drop destinations in the presenting view can be detected.

As the OP pointed out, it is possible to detect when a drag gesture starts, but a better workaround I found is using DropDelegate. The basic idea is to attach a custom delegate to the popover that detects when the dragged view exits its bounds, allowing us to control when we want to dismiss it.

/// A work-around thst allows `.draggable` views presented within a `.popover` modal
/// to be dropped outside of the popover.
/// ```swift
///  NavigationStack {
///      Text("Drop Zone")
///          .dropDestination(for: String.self) { items, location in
///              true
///          } isTargeted: {
///              print("isTargeted \($0)")
///          }
///          .toolbar {
///              ToolbarItem(placement: .topBarTrailing) {
///                  Button(“Press Me”) {
///                      isPresented = true
///                  }
///                  .popover(isPresented: $isPresented) {
///                      NavigationStack {
///                          ZStack {
///                              Color.green.ignoresSafeArea()
///                              Color.red.frame(width: 50, height: 50).draggable("Payload")
///                          }
///                          .navigationTitle("Modal")
///                      }
///                      .frame(minWidth: 300, minHeight: 500)
///                      .onDrop(of: [.plainText], delegate: ExitDropDelegate(onDropExit: { isPresented = false }))
///                  }
///              }
///          }
///  }
/// ```
/// In this example, the drop zone in the outer `NavigationStack` does not detect the draggable
/// view hovering over it, due to the popover being modally presented. The `ExitDropDelegate`
/// dismisses the popover when the view is dragged outside of its bounds, allowing the drop zone to
/// be targeted.
struct ExitDropDelegate: DropDelegate {
    var onDropExit: (() -> Void)? = nil
    
    func performDrop(info: DropInfo) -> Bool { false }
    
    func dropUpdated(info: DropInfo) -> DropProposal? {
        /// Hides the green plus button
        .init(operation: .move)
    }
    
    func dropExited(info: DropInfo) { onDropExit?() }
}

This approach can be extended with more complex logic, for example if we want to dismiss the popover only after the user hovers the view outside of the popover for a certain period of time, then we can add a timer to the DropDelegate that starts when the dropExited is called, and invalidated when dropEntered is called before the timer fires.