动画偏移正在破坏按钮 SwiftUI

nic*_*ng2 5 animation swift swiftui

我在堆栈中有一些带有动画偏移的按钮。由于某种原因,动画偏移按钮无法单击。当偏移量约为 250 左右时,按钮似乎可以单击一秒钟,然后在偏移量低于该值时再次变得不可单击...非常感谢任何帮助!

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
            }
        })
    }
}   
Run Code Online (Sandbox Code Playgroud)

gra*_*irr 2

抵消如何运作?

首先,这是预期的行为。因为当您使用时offsetSwiftUI 显示的内容会发生变化。简而言之,这意味着SwiftUI 改变View自身

由于onTapGesture 仅识别视图上的触摸,这也解释了为什么您可以单击偏移视图

抵消

动画如何运作?

在你的代码中,你是offsetting你的View第一个,然后你正在应用你的动画。当您使用 时withAnimationSwiftUI 会使用提供的动画重新计算视图的主体,但请记住,它不会更改之前应用于的任何内容View

动画片

请注意Click Me进入红色矩形时如何变得可点击。发生这种情况是因为红色矩形表示Click Me按钮的最终偏移量。(所以它只是一个占位符)

因此View,它本身和offset必须匹配,因为当您首先偏移视图时,SwiftUI需要您的视图在那里触发点击手势。

可能的解决方案

现在我们了解了问题,我们就可以解决它了。所以,问题的发生是因为我们首先偏移视图,然后应用动画

因此,如果这没有帮助,一种可能的解决方案可能是通过动画更改周期内的偏移量(例如,我每个周期使用 0.1 秒),因为这会导致 SwiftUI 在每次更改偏移量时重新定位视图,因此我们奇怪的错误不应该发生。

代码:

struct ContentView: View {
    @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
    var timerPublishInSeconds : TimeInterval = 0.1
    let timer : Publishers.Autoconnect<Timer.TimerPublisher>
    var autoreverses : Bool = false
    
    @State private var decreasing = false
    
    public init(offset: Binding<CGFloat>, duration: Double, finalOffsetAmount: Double, animation: Animation, autoreverses: Bool) {
        self._offset = offset
        self.duration = duration
        self.finalOffsetAmount = finalOffsetAmount
        self.animation = animation
        self.autoreverses = autoreverses
        
        self.timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
    }
    
    public init(offset: Binding<CGFloat>, duration: Double, finalOffsetAmount: Double, animation: Animation, timerPublishInSeconds: TimeInterval) {
        self._offset = offset
        self.duration = duration
        self.finalOffsetAmount = finalOffsetAmount
        self.animation = animation
        self.timerPublishInSeconds = timerPublishInSeconds
        
        self.timer = Timer.publish(every: timerPublishInSeconds, on: .main, in: .common).autoconnect()

    }
    
    public init(offset: Binding<CGFloat>, duration: Double, finalOffsetAmount: Double, animation: Animation) {
        self._offset = offset
        self.duration = duration
        self.finalOffsetAmount = finalOffsetAmount
        self.animation = animation
        
        self.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.
                    if autoreverses {
                        self.decreasing = true
                    }
                    else {
                        timer.upstream.connect().cancel()
                        return
                    }
                }
                
                if offset <= 0 {
                    self.decreasing = false
                }
                
                if decreasing {
                    offset -= finalOffsetAmount / (duration / timerPublishInSeconds)
                }
                else {
                    offset += finalOffsetAmount / (duration / timerPublishInSeconds)
                }
            }
    }
}
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))
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑:我添加了可自定义的时间戳以及自动反转。

它看起来是这样的:

解决方案

你的视图正在运行,去捕捉它 x)