【问题标题】:SwiftUI withAnimation completion callbackSwiftUI withAnimation 完成回调
【发布时间】:2020-01-05 21:22:04
【问题描述】:

我有一个基于某种状态的 swiftUI 动画:

withAnimation(.linear(duration: 0.1)) {
    self.someState = newState
}

上面的动画完成时有没有触发回调?

如果有任何关于如何在 SwiftUI 中使用完成块而不是 withAnimation 来完成动画的建议,我也愿意接受。

我想知道动画何时完成,以便我可以做其他事情,就本示例而言,我只想在动画完成时打印到控制台。

【问题讨论】:

  • 不幸的是,目前 SwiftUI 没有在动画结束时为您提供回调,无论是隐式动画还是显式动画(如您的示例)。
  • 令人难以置信的 SwiftUI 还没有这个。动画生命周期中如此重要的一部分。有人知道这方面有什么新的吗?

标签: ios swift animation swiftui ios13


【解决方案1】:

Guy Javier 在这个博客上描述了如何使用 GeometryEffect 来获得动画反馈,在他的示例中,他检测到动画何时达到 50%,因此他可以翻转视图并使其看起来像有 2 个边

这里有很多解释的全文链接:https://swiftui-lab.com/swiftui-animations-part2/

我将在此处复制相关的 sn-ps,因此即使链接不再有效,答案仍然可以相关:

在此示例中,@Binding var flipped: Bool 在角度介于 90 和 270 之间时变为真,然后变为假。

struct FlipEffect: GeometryEffect {

    var animatableData: Double {
        get { angle }
        set { angle = newValue }
    }

    @Binding var flipped: Bool
    var angle: Double
    let axis: (x: CGFloat, y: CGFloat)

    func effectValue(size: CGSize) -> ProjectionTransform {

        // We schedule the change to be done after the view has finished drawing,
        // otherwise, we would receive a runtime error, indicating we are changing
        // the state while the view is being drawn.
        DispatchQueue.main.async {
            self.flipped = self.angle >= 90 && self.angle < 270
        }

        let a = CGFloat(Angle(degrees: angle).radians)

        var transform3d = CATransform3DIdentity;
        transform3d.m34 = -1/max(size.width, size.height)

        transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0)
        transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)

        let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))

        return ProjectionTransform(transform3d).concatenating(affineTransform)
    }
}

您应该能够将动画更改为您想要实现的任何内容,然后在完成后获取绑定以更改父级的状态。

【讨论】:

    【解决方案2】:

    您需要使用自定义修饰符。

    我已经做了一个例子,用一个完成块为 X 轴上的偏移设置动画。

    struct OffsetXEffectModifier: AnimatableModifier {
    
        var initialOffsetX: CGFloat
        var offsetX: CGFloat
        var onCompletion: (() -> Void)?
    
        init(offsetX: CGFloat, onCompletion: (() -> Void)? = nil) {
            self.initialOffsetX = offsetX
            self.offsetX = offsetX
            self.onCompletion = onCompletion
        }
    
        var animatableData: CGFloat {
            get { offsetX }
            set {
                offsetX = newValue
                checkIfFinished()
            }
        }
    
        func checkIfFinished() -> () {
            if let onCompletion = onCompletion, offsetX == initialOffsetX {
                DispatchQueue.main.async {
                    onCompletion()
                }
            }
        }
    
        func body(content: Content) -> some View {
            content.offset(x: offsetX)
        }
    }
    
    struct OffsetXEffectModifier_Previews: PreviewProvider {
      static var previews: some View {
        ZStack {
          Text("Hello")
          .modifier(
            OffsetXEffectModifier(offsetX: 10, onCompletion: {
                print("Completed")
            })
          )
        }
        .frame(width: 100, height: 100, alignment: .bottomLeading)
        .previewLayout(.sizeThatFits)
      }
    }
    
    

    【讨论】:

      【解决方案3】:

      这里有一点简化和通用的版本,可用于任何单值动画。这是基于我在等待 Apple 提供更方便的方式时在互联网上找到的其他一些示例:

      struct AnimatableModifierDouble: AnimatableModifier {
      
          var targetValue: Double
      
          // SwiftUI gradually varies it from old value to the new value
          var animatableData: Double {
              didSet {
                  checkIfFinished()
              }
          }
      
          var completion: () -> ()
      
          // Re-created every time the control argument changes
          init(bindedValue: Double, completion: @escaping () -> ()) {
              self.completion = completion
      
              // Set animatableData to the new value. But SwiftUI again directly
              // and gradually varies the value while the body
              // is being called to animate. Following line serves the purpose of
              // associating the extenal argument with the animatableData.
              self.animatableData = bindedValue
              targetValue = bindedValue
          }
      
          func checkIfFinished() -> () {
              //print("Current value: \(animatableData)")
              if (animatableData == targetValue) {
                  //if animatableData.isEqual(to: targetValue) {
                  DispatchQueue.main.async {
                      self.completion()
                  }
              }
          }
      
          // Called after each gradual change in animatableData to allow the
          // modifier to animate
          func body(content: Content) -> some View {
              // content is the view on which .modifier is applied
              content
              // We don't want the system also to
              // implicitly animate default system animatons it each time we set it. It will also cancel
              // out other implicit animations now present on the content.
                  .animation(nil)
          }
      }
      

      下面是一个关于如何将它与文本不透明动画一起使用的示例:

      import SwiftUI
      
      struct ContentView: View {
      
          // Need to create state property
          @State var textOpacity: Double = 0.0
      
          var body: some View {
              VStack {
                  Text("Hello world!")
                      .font(.largeTitle)
      
                       // Pass generic animatable modifier for animating double values
                      .modifier(AnimatableModifierDouble(bindedValue: textOpacity) {
      
                          // Finished, hurray!
                          print("finished")
      
                          // Reset opacity so that you could tap the button and animate again
                          self.textOpacity = 0.0
      
                      }).opacity(textOpacity) // bind text opacity to your state property
      
                  Button(action: {
                      withAnimation(.easeInOut(duration: 1.0)) {
                          self.textOpacity = 1.0 // Change your state property and trigger animation to start
                      }
                  }) {
                      Text("Animate")
                  }
              }
          }
      }
      
      struct HomeView_Previews: PreviewProvider {
          static var previews: some View {
              ContentView()
          }
      }
      

      【讨论】:

        【解决方案4】:

        很遗憾,这个问题还没有很好的解决方案。

        但是,如果您可以指定Animation 的持续时间,您可以使用DispatchQueue.main.asyncAfter 在动画结束时准确触发动作:

        withAnimation(.linear(duration: 0.1)) {
            self.someState = newState
        }
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            print("Animation finished")
        }
        

        【讨论】:

          【解决方案5】:

          你可以试试VDAnimation library

          Animate(animationStore) {
              self.someState =~ newState
          }
          .duration(0.1)
          .curve(.linear)
          .start {
              ...
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2021-03-17
            • 1970-01-01
            • 2019-03-23
            • 2018-05-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多