How to make `.draggable` preview the same as the view being dragged in SwiftUI

107 Views Asked by At

I have a bit of confusion on the .draggable modifier in SwiftUI. I'm playing around with the overload that lets us pass in a view for the drag preview, but I cannot figure out how the framing/layout works for this preview. Below is the code I'm messing with:

@State private var items: [Color] = [.red, .blue, .green, .orange, .purple]

var body: some View {
  VStack(spacing: 16) {
    ForEach(items, id: \.self) { color in
      color
        .frame(height: 100)
        .draggable(color) {
          color
            .frame(height: 100)
        }
    }
  }
  .padding()
}

and here is the result:

enter image description here

As you can see, the preview barely takes any width, and the height is clearly much greater than the 100 height of the color in the VStack. Why is this? Is there something about the preview's context that changes the framing behavior?

Furthermore, is there a way to set the frame exactly to that of the original view? Or even more ideally, is there a way to set the preview to exactly the view of the original (in this case just the color with the same frame), so that it looks like we're dragging the original view?

1

There are 1 best solutions below

14
MatBuompy On

It would seem the preview of the draggable modifier is interfering with size of the View. My personal solution would be to simulate the dragging behavior by using a combination of gestures and overlay while still keeping the draggable modifier.

Here's an example based on your code. First I've declared a struct conforming to Identifiable (to distinguish the selected one from the others) holding a color:

struct DraggableColor: Identifiable {
    let id = UUID()
    let color: Color
}

And the View is as follows:

    @State private var text = ""
    @State private var draggedColor: DraggableColor?
    @State private var offset: CGSize = .zero
    var draggableColors: [DraggableColor] = [.init(color: .blue), .init(color: .red), .init(color: .green), .init(color: .yellow)]
    
    var body: some View {
        VStack(spacing: 16) {
            ForEach(draggableColors) { color in
                color.color
                    .frame(height: 100)
                    .draggable(color.color) {}
                    .overlay {
                        color.color
                            .frame(height: 100)
                            /// To only make the selected color move
                            .offset(x: draggedColor?.id == color.id ? offset.width : 0,
                                    y: draggedColor?.id == color.id ? offset.height : 0)
                            .gesture(
                                /// Long Press gesture to simulate the draggable press minimum duration
                                LongPressGesture(minimumDuration: 0.3).onEnded({ value in
                                    draggedColor = color
                                })
                                .simultaneously(with: DragGesture(minimumDistance: 0)
                                    .onChanged({ value in
                                        guard draggedColor != nil else { return }
                                        offset = value.translation
                                    })
                                    .onEnded({ value in
                                        withAnimation(.smooth) {
                                            offset = .zero
                                        }
                                    }))
                            )
                    }
                    /// Since the the colors appearing first go behind the colors appearing next
                    .opacity((offset != .zero && draggedColor?.id != color.id) ? 0.1 : 1)
            }
        }
        .frame(maxWidth: .infinity)
        .padding()
    }

I've used a LongPressGesture to simulate the prolongued tap to trigger the draggable modifier. I've set it to 0.3s, you can change it to fit your needs, of course.

This is simply a workaround, of course. If you find a solution for the native modifier it would be better and less complicated then this. Among the issues I've found is that when dragging colors they will go behind the ones appearing below, that's why I've used the opacity. I also tried used the zIndex but with no luck.

Here's the result:

Draggable Colors

Let me know if your liks this solution!