0

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)'")
        }
    }
}
4
  • Have you tried something like: .gesture(DragGesture().onChanged({ value in }) .simultaneously(with: TapGesture().onEnded({ _ in })))?
    – MatBuompy
    Commented Feb 27 at 14:10
  • @MatBuompy I have tried similar options, but none have worked as desired for drag/drop. I tried the code you gave above just now, and the objects didn't respond to any gestures.
    – Amone1
    Commented Feb 28 at 6:43
  • Can you provide a minimal reproducible example?
    – MatBuompy
    Commented Feb 28 at 8:07
  • @MatBuompy Added minimal reproducible example to question.
    – Amone1
    Commented Feb 28 at 14:32

1 Answer 1

0

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.

4
  • I like what you have whipped up. It's fine that it only works for strings, since I only intend to use it for strings. If the string is dragged over To here its value remains as To here even if dropped onto white space (it only changes back to Drag me if dragged over Drag me again). Ideally, the dragInfo reverts back to its original content if not over a drop location. I see that it isn't flexible, since text = "Drag me" is 'baked in' rather than using the original dragInfo. I'll try work on this more, and curious to see if you have any breakthroughs too.
    – Amone1
    Commented Feb 29 at 3:42
  • I've modified the code so that is a bit more general. It can keep track of the string that was dropped, and also the string of the drop end location (both are important pieces of info for my use). However, having multiple strings on the screen which can be dragged causes issues, since the dragLocation and dragTranslation are both @Binding variables, so are shared across all strings. This causes all strings to appear to move with the cursor, regardless of which one is actually being dragged. Unsure how to fix.
    – Amone1
    Commented Feb 29 at 6:23
  • Maybe make those optional? Let me know
    – MatBuompy
    Commented Feb 29 at 7:48
  • Make those optional and also use an array of State variables. I don't see any other way of doing this. You need to keep track of thir postion on the screen. You maybe can avoid dragTranslation but then you'll lose the drag effect on screen. If you post your updated version of this code maybe I can try to improve it.
    – MatBuompy
    Commented Feb 29 at 8:22

Not the answer you're looking for? Browse other questions tagged or ask your own question.