How to keep drag preview exactly the same as original item?

96 Views Asked by At

Consider following simplified code for an element that can be dragged

    HStack {
      Icon(icon: .iconDrag)
      Text("Title")
      Spacer()
    }
    .padding(16)
    .onDrag {
      return NSItemProvider(object: id.uuidString as NSString)
    }

This results in behavior below, where once drag starts preview element is shown, but it is a fraction of a size from original element and has transparency.

I tried adding preview : {} to onDrag with same original element duplicate, but it's size is still inaccurate and it has transparency. Is there a way to extend a preview somehow (say with uikit) so that it remains exactly the same as original element? I believe there is a maximum width of preview element at which point it starts shrinking down perhaps?

enter image description here

1

There are 1 best solutions below

1
VonC On BEST ANSWER

You could try implementing a custom drag preview by adopting the UIDragInteractionDelegate in a UIViewRepresentable that wraps your SwiftUI view: by using UIDragPreview and setting the preview provider of the UIDragItem, you can specify a custom view for the preview that will be the same size as the original view and without any transparency.

+---------------------------------+
| +----+ +-------------------+    |
| |Icon| |Title              |    |
| +----+ +-------------------+    |
| | UIViewRepresentable wrapper   |
| +-------------------------------+
| | UIDragInteractionDelegate     |
+---------------------------------+
   |                                  
   | Custom drag behavior             
   | (UIDragInteractionDelegate)      
   v                                  
+----------------------------------+   
| UIDragPreview (Custom preview)   |   
| Same size, no transparency       |   
+----------------------------------+   

This is inspired from Client/Frontend/TabChrome/TopBar/LocationView/LocationViewTouchHandler.swift

import SwiftUI
import UIKit

// Wrap your SwiftUI view in a UIViewRepresentable
struct DraggableView: UIViewRepresentable {
    var text: String
    
    func makeUIView(context: Context) -> UIView {
        let view = UIHostingController(rootView: DraggableTextView(text: text)).view!
        view.backgroundColor = .clear
        let dragInteraction = UIDragInteraction(delegate: context.coordinator)
        view.addInteraction(dragInteraction)
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    class Coordinator: NSObject, UIDragInteractionDelegate {
        func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
            let provider = NSItemProvider(object: "Dragged content" as NSString)
            let item = UIDragItem(itemProvider: provider)
            item.previewProvider = { return UIDragPreview(view: interaction.view!) }
            return [item]
        }
    }
}

// Your original SwiftUI view
struct DraggableTextView: View {
    var text: String
    
    var body: some View {
        HStack {
            Image(systemName: "hand.point.up.left.fill") // Example icon
            Text(text)
            Spacer()
        }
        .padding(16)
        .background(Color.gray) // For demo purposes
    }
}

struct ContentView: View {
    var body: some View {
        DraggableView(text: "Draggable Text")
    }
}

The key part in the code which should allow for a "Same size, no transparency" drag preview is the previewProvider closure of UIDragItem.

item.previewProvider = { return UIDragPreview(view: interaction.view!) }

The UIDragPreview initializer is passed the interaction.view, which is the UIView that was created by the UIHostingController with the SwiftUI view inside of it. Since you are using the original view itself for the preview, it should have the same size as the original.