Home > Software design >  Offset with animation is breaking buttons SwiftUI
Offset with animation is breaking buttons SwiftUI

Time:10-31

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 offset

How Animation Works?

In your code, you're offsetting your View First, then you're applying your animation. When you use withAnimation, animation

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:

solution

Your view is running, go catch it out x)

  • Related