I'm trying to get a "true" value when the user uses gestures to move the 300x300 rectangle and a "false" value when they finish interacting with gestures.
In this case it seemed logical to use @GestureState
As you can see in the .updating()
call inside DragGesture
I used @GestureState private var isDragging: Bool = false
@GestureState
should change the opacity of the 100x100 red rectangle
public struct ContentView: View {
@State private var offset: CGSize = .zero
@State private var lastOffset: CGSize = .zero
@GestureState private var isDragging: Bool = false
public init() { }
public var body: some View {
VStack(spacing: 40) {
Rectangle().fill(.red)
.frame(width: 100, height: 100)
.opacity(isDragging ? 0 : 1)
Rectangle()
.frame(width: 300, height: 300)
.offset(offset)
.gesture(
DragGesture()
.updating($isDragging, body: { _, out, _ in
out = true
})
.onChanged({ value in
let currentOffset: CGSize = .init(width: lastOffset.width + value.translation.width, height: lastOffset.height + value.translation.height)
offset = currentOffset
})
.onEnded({ _ in
lastOffset = offset
})
)
}
.animation(.default, value: isDragging)
}
}
everything seems to work fine but when I add .animation(.default, value: isDragging)
the console gives me this error while I use the dragGesture
and the red rectangle opacity animation is running
Invalid sample AnimatablePair<AnimatablePair<CGFloat, CGFloat>, AnimatablePair<CGFloat, CGFloat>>(first: SwiftUI.AnimatablePair<CoreGraphics.CGFloat, CoreGraphics.CGFloat>(first: -23.666666666666664, second: -19.0), second: SwiftUI.AnimatablePair<CoreGraphics.CGFloat, CoreGraphics.CGFloat>(first: 0.0, second: 0.0)) with time Time(seconds: 0.0) > last time Time(seconds: 0.016666666720993817)
and I don't understand why... if I don't use animations everything works correctly otherwise it tells me this problem...
Where am I wrong? isn't this the right way to animate objects based on the use of gestures? are there other ways? is this the wrong way?
I found that it helps to add animation to other state changes affected by the drag gesture. In your example, this means animating changes to offset
too.
Here are two possible ways to solve:
1. Add .animation
modifiers for other variables
For the example here, add a modifier for offset
:
VStack(spacing: 40) {
//...
}
.animation(.default, value: isDragging)
.animation(.easeInOut(duration: 0.1), value: offset) // 👈 here
If you don't really want any additional animation then a duration of 0 seems to work. However, supplying nil as the animation type does not work and still reports errors in the console.
2. Perform changes using withAnimation
An alternative way to solve is to use withAnimation
for updates performed in gesture callbacks, where these changes are not already covered by .animation
modifiers:
.onChanged({ value in
let currentOffset: CGSize = .init(width: lastOffset.width + value.translation.width, height: lastOffset.height + value.translation.height)
withAnimation(.easeInOut(duration: 0.1)) { // 👈 here
offset = currentOffset
}
})
As in point 1, a duration of 0 can be used, but an animation type of nil does not work and still reports errors in the console. Performing the changes using a Transaction
with disablesAnimations = true
also reports errors.
You were asking:
isn't this the right way to animate objects based on the use of gestures? are there other ways? is this the wrong way?
I would suggest, it is more conventional to use a GestureState
for a value that is changing during the gesture, rather than for a boolean flag that signals that the gesture is happening. This avoids the need for an additional .onChanged
modifier.
Here is how your example could be re-factored by using a GestureState
for the drag offset. Things to note:
The boolean flag isDragging
is derived from the drag offset (set to true if the offset is non-zero).
The animation modifiers are applied more specifically.
The callbacks for the drag gesture use trailing closure syntax.
public struct ContentView: View {
@State private var offset: CGSize = .zero
@GestureState private var dragOffset = CGSize.zero
private var isDragging: Bool {
dragOffset != .zero
}
private func combinedOffset(adding: CGSize) -> CGSize {
CGSize(
width: offset.width + adding.width,
height: offset.height + adding.height
)
}
public var body: some View {
VStack(spacing: 40) {
Rectangle()
.fill(.red)
.frame(width: 100, height: 100)
.opacity(isDragging ? 0 : 1)
.animation(.default, value: isDragging)
Rectangle()
.frame(width: 300, height: 300)
.offset(combinedOffset(adding: dragOffset))
.animation(.easeInOut(duration: 0.1), value: dragOffset)
.gesture(
DragGesture()
.updating($dragOffset) { val, out, _ in
out = val.translation
}
.onEnded { val in
offset = combinedOffset(adding: val.translation)
}
)
}
}
}