How to make .draggable() work with click+drag rather than long-press+drag?

61 Views Asked by At

I have been programming an app in SwiftUI which heavily uses the in-built .draggable() and .dropDestination() modifiers.

To enhance the user experience, I would like to make .draggable() work with click+drag rather than long-press+drag (so it is faster to use and less clunky).

I come from Python, where I am used to being able to find system functions and copy them then edit them if I require (under a new function name, of course).
Is it possible to do the same thing with .draggable() and .dropDestination() in SwiftUI, so that I may edit what gesture activates the drag/drop, whilst retaining all other functionality?

Alternatively if I can make an extension to the functions instead that would be an option.

I have looked into writing my own drag/drop gestures, however much of my program relies on the in-built functionality of the .draggable() and .dropDestination() modifiers themselves. This is functionality I have not been able to replicate.

My research on the topic / these functions before coming here has shined little light on the subject, and GPT has not been helpful either.

Minimum reproducible example, simulating the functionality I am currently using drag/drop for.

struct TestView3: View {
    @State var ee: String = ""
    var body: some View {
        VStack {
            Text("Drag me")
                .draggable("Drag me")
                .padding()
            Text("to here")
                    .dropDestination(for: String.self) { droppedObjects, location in
                        ee = droppedObjects[0]
                        return true
                    }
                    .padding()
            Text("You dropped: '\(ee)'")
        }
    }
}
1

There are 1 best solutions below

4
MatBuompy On

It has been a bit tough to achieve, and I don't know if it is even worth using. Maybe with some changes it can be scalable, but now it only works with Strings. I built an extension and a modifier to make things draggable creating an overlay to simulate the effect of the default draggable modifier:

struct DragModifier: ViewModifier {
    
    var tag: String
    
    @Binding var dragLocation: CGPoint
    
    /// To make the text move
    @Binding var dragTranslation: CGSize
    
    /// To keep track of the current cursor location
    @Binding var dragInfo: String
    
    
    func body(content: Content) -> some View {
        content
            .background { dragDetector(for: tag) }
    }
    
    private func dragDetector(for name: String) -> some View {
        GeometryReader { proxy in
            let frame = proxy.frame(in: .global)
            let isDragLocationInsideFrame = frame.contains(dragLocation)
            let isDragLocationInsideArea = isDragLocationInsideFrame &&
            Circle().path(in: frame).contains(dragLocation)
            Color.clear
                .onChange(of: isDragLocationInsideArea) { oldVal, newVal in
                    if dragLocation != .zero {
                        dragInfo = name
                    }
                }
        }
    }
    
}

extension View {
    
    @ViewBuilder
    func makeDraggable(tag: String, dragLocation: Binding<CGPoint>,
                       dragTranslation: Binding<CGSize>, dragInfo: Binding<String>, canBeDragged: Bool = true) -> some View {
        self
            .modifier(DragModifier(tag: tag, dragLocation: dragLocation, dragTranslation: dragTranslation, dragInfo: dragInfo))
            .overlay {
                if canBeDragged {
                    Text(tag)
                        .offset(x: dragTranslation.wrappedValue.width, y: dragTranslation.wrappedValue.height)
                        .opacity(dragTranslation.wrappedValue != .zero ? 1 : 0)
                }
            }
    }
    
}

Here dragLocation is used to track where the user's finger or cursor is, while dragTranslation is used to make the overlayed text move.

Here's an example on how you can use it:

struct DragView: View {
    
    @State private var dragLocation = CGPoint.zero
    
    /// To make the text move
    @State private var dragTranslation = CGSize.zero
    
    /// To keep track of the current cursor location
    @State private var dragInfo = " "
    
    /// The text being dragged
    @State private var text: String = ""
    @State private var startedFrom: Bool = false
    
    var body: some View {
        ZStack {
            VStack(spacing: 50) {
                
                Text(dragInfo)
                
                Text("Drag me")
                    .padding()
                    .frame(width: 100, height: 100)
                    .makeDraggable(
                        tag: "Drag Me",
                        dragLocation: $dragLocation,
                        dragTranslation: $dragTranslation,
                        dragInfo: $dragInfo
                    )
                
                Text("To here")
                    .frame(width: 100, height: 50)
                    .makeDraggable(tag: "To Here",
                                   dragLocation: $dragLocation,
                                   dragTranslation: $dragTranslation,
                                   dragInfo: $dragInfo,
                                   canBeDragged: false)
                    .background(dragInfo == "To Here" ? Color.blue.opacity(0.4) : Color.clear)
                    .clipShape(.rect(cornerRadius: 4))
                
                Text("You dropped: '\(text)'")
                    .frame(width: 300, height: 100)
            }
        }
        .gesture(
            DragGesture(coordinateSpace: .global)
                .onChanged { val in
                    dragLocation = val.location
                    print("Drag Info: \(dragInfo) - Drag Trans \(dragTranslation)")
                    /// Change this to fit  your needs
                    if dragInfo.lowercased().contains("drag me") && dragTranslation == .zero {
                        startedFrom = true
                    }
                    if startedFrom {
                        dragTranslation = val.translation
                        print("Drag Trans")
                    }
                }
                .onEnded { val in
                    withAnimation(.smooth) {
                        if dragInfo.lowercased().contains("to here") && startedFrom {
                            text = "Drag me"
                        }
                        dragTranslation = .zero
                    }
                    dragLocation = .zero
                    dragInfo = " "
                    startedFrom = false
                }
        )
    }
}

Be aware that it definitely needs some work to make it really usable. Now it isn't really flexible. Maybe tomorrow I can work on it a bit more.

Here's the result:

Drag and Drop

Let me know what do you think of this.