I have some buttons inside a stack with an animated offset. For some reason, with the animated offset buttons, they are not clickable. The buttons seem to be clickable for a second when offset is about 250 or so and then become non-clickable at offsets below that value again...Any help is much appreciated!
struct ContentView: View {
@State var offset: CGFloat = -300
var body: some View {
HStack {
Button(action: {
print("clickable")
}, label: {
Text("Click me")
})
Button(action: {
print("clickable2")
}, label: {
Text("Click me2")
})
Button(action: {
print("clickable3")
}, label: {
Text("Click me3")
})
}.offset(x: offset)
.onAppear(perform: {
withAnimation(.linear(duration: 10).repeatForever()) {
offset = 300
}
})
}
}
CodePudding user response:
How Offsetting works?
First of all, this is an expected behavior. Because when you use offset
, SwiftUI
How Animation Works?
In your code, you're offsetting
your View
First, then you're applying your animation. When you use withAnimation
,
Notice how Click Me
becomes clickable when entering the red rectangle. That happens because the red rectangle indicates the final offset amount of the Click Me
button. (so it is just a placeholder)
So the View
itself, and the offset
has to match because as you offset your View first, SwiftUI
needs your view there to trigger the tap gestures.
Possible solution
Now that we understand the problem, we can solve it. So, the problem happens because we are offsetting our view first, then applying animation.
So if that does not help, one possible solution could be to change the offset in periods (for example, I used 0.1 seconds per period) with an animation, because that would result in SwiftUI repositioning the view every time we change the offset, so our weird bug should not occur.
Code:
struct ContentView: View {
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
@State private var increment : CGFloat = 1
@State private var offset : CGFloat = 0
var body: some View {
ZStack {
Button("Click Me") {
print("Click")
}
.fontWeight(.black)
}
.tappableOffsetAnimation(offset: $offset, animation: .linear, duration: 5, finalOffsetAmount: 300)
}
}
struct TappableAnimationModifier : ViewModifier {
@Binding var offset : CGFloat
var duration : Double
var finalOffsetAmount : Double
var animation : Animation
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
func body(content: Content) -> some View {
content
.animation(animation, value: offset)
.offset(x: offset)
.onReceive(timer) { input in
/*
* a simple math here, we're dividing duration by 0.1 because our timer gets triggered
* in every 0.1 seconds, so dividing this result will always produce the
* proper value to finish offset animation in `x` seconds
* example: 300 / (5 / 0.1) = 300 / 50 = 6 increment per 0.1 second
*/
if (offset >= finalOffsetAmount) {
// you could implement autoReverses by not canceling the timer here
// and substracting finalOffsetAmount / (duration / 0.1) until it reaches zero
// then you can again start incrementing it.
timer.upstream.connect().cancel()
return
}
offset = finalOffsetAmount / (duration / 0.1)
}
}
}
extension View {
func tappableOffsetAnimation(offset: Binding<CGFloat>, animation: Animation, duration: Double, finalOffsetAmount: Double) -> some View {
modifier(TappableAnimationModifier(offset: offset, duration: duration, finalOffsetAmount: finalOffsetAmount, animation: animation))
}
}
Here's how it looks like:
Your view is running, go catch it out x)