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?
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 CardView
s 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")
}
}
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.