How to detect when SwiftUI View is being dragged over
Asked Answered
D

3

7

I would like to trigger an animation when the user drags a finger over my view.

I can do something like

Image(…)
  .rotationEffect(…)
  .animation(self.isAnimating ? .spring : .default)
  .gesture(
    DragGesture(minimumDistance: 5, coordinateSpace: .global)
      .onChanged { value in
        self.isAnimating = true
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
          self.isAnimating = false
        }
      }
  )

However that captures only a drag event that started over the image. A drag event that started elsewhere and then travels over the image is ignored.

I can also detect the drag events in the parent view and calculate which of the child views is being dragged over – and that's fine. However, how do I tell the child view to animate then? Updating their properties causes a re-render which of course cancels the animation.

Passing ui data like this through a model seems like an anti pattern.

Any suggestions?

Destruction answered 14/8, 2021 at 17:18 Comment(2)
Did you find the answers helpful, or do you need more help?Uyekawa
See also Is it possible to detect which View currently falls under the location of a DragGesture?Broth
U
19

I replaced the Image of your example by a Grid of small CardViews. We will try to change the color of the cards that are "crossed" by the drag gesture.

We can use PreferenceKey to get all the CardViews bounds...

struct CardPreferenceData: Equatable {
    let index: Int
    let bounds: CGRect
}

struct CardPreferenceKey: PreferenceKey {
    typealias Value = [CardPreferenceData]
    
    static var defaultValue: [CardPreferenceData] = []
    
    static func reduce(value: inout [CardPreferenceData], nextValue: () -> [CardPreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}

here :

struct CardView: View {
    let index: Int
    
    var body: some View {
        Text(index.description)
            .padding(10)
            .frame(width: 60)
            .overlay(RoundedRectangle(cornerRadius: 10).stroke())
            .background(
                GeometryReader { geometry in
                    Rectangle()
                        .fill(Color.clear)
                        .preference(key: CardPreferenceKey.self,
                                    value: [CardPreferenceData(index: self.index, bounds: geometry.frame(in: .named("GameSpace")))])
                }
            )
    }
}

In the ContentView now we can collect all preferences (bounds and index) of these cards and store them in an array :

.onPreferenceChange(CardPreferenceKey.self){ value in
            cardsData = value
        }

We can now compare the positions (the bounds) of these CardViews to the position of the drag gesture.


struct ContentView: View {
    let columns = Array(repeating: GridItem(.fixed(60), spacing: 40), count: 3)
    @State private var selectedCardsIndices: [Int] = []
    @State private var cardsData: [CardPreferenceData] = []
    var body: some View {
        LazyVGrid(columns: columns, content: {
            ForEach((1...12), id: \.self) { index in
                CardView(index: index)
                    .foregroundColor(selectedCardsIndices.contains(index) ? .red : .blue)
            }
        })
        .onPreferenceChange(CardPreferenceKey.self){ value in
            cardsData = value
        }
        .gesture(
            DragGesture()
                .onChanged {drag in
                    if let data = cardsData.first(where: {$0.bounds.contains(drag.location)}) {
                        selectedCardsIndices.append(data.index)
                    }
                }
        )
        .coordinateSpace(name: "GameSpace")
    }
}

Example

EDIT : The small "lag" at the start of the video does not occur with the canvas. Only on the simulator. I have not tested on a real device.

Uyekawa answered 14/8, 2021 at 18:33 Comment(3)
how to give a line to link that?Strawworm
God, why such seemingly easy things are so complicated 😝 I pray every year before WWDC that Apple will do sth with that 🙏 Until then, +1 for you. Thanks for sharing!Strobotron
This helped me so much. Never even heard about priority keys. Super useful! At least after a few tutorials on what and how it worked. Helped me implement my own custom drag and drop!Annal
I
0

How about this way?

I set the drag over point at +30/-30 at both ends.

e.g. iPhone 12 width: 844, Endpoint: x: 30 and x: 814




struct ContentView: View {
    
    @State private var offsetX: CGFloat = 0
    @State private var offsetY: CGFloat = 0
    
    var body: some View {
        VStack {
            Circle()
                .frame(width: 100, height: 100)
                .offset(x: offsetX, y: offsetY)
                .gesture(DragGesture(minimumDistance: 5, coordinateSpace: .global)
                            .onChanged { value in
                                offsetX = value.translation.width
                                offsetY = value.translation.height
                                
                                print("x: \(value.location.x)")
                                print("y: \(value.location.y)")
                                
                                if value.location.x > UIScreen.main.bounds.width - 30 || value.location.x < 30 {
                                    print("End")
                                }
                                
                                if value.location.y > UIScreen.main.bounds.height - 30 || value.location.y < 30 {
                                    print("End")
                                }
                            }
                )
        }
    }
}



enter image description here

Impower answered 14/8, 2021 at 18:16 Comment(0)
C
-1

This worked for me:

                .gesture(
                    DragGesture()
                        .onChanged { drag in
                            print("drag: \(drag)")
                            userIsDraggingMap = true
                        }
                        .onEnded { drag in
                            userIsDraggingMap = false
                        }
                )
Carolacarolan answered 24/5, 2022 at 2:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.