💡 SwiftUI:隐式动画与显式动画的区别

为您每周带来有关 Swift 和 SwiftUI 的精选资讯!

TL;DR: SwiftUI 提供隐式动画(通过 .animation 声明)和显式动画(通过 withAnimationwithTransaction 定义)两种机制。隐式动画适合细化的效果控制,显式动画用于集中管理状态变化的动画需求。

背景

SwiftUI 提供了隐式动画和显式动画两种机制。许多开发者会困惑于它们的区别及适用场景。本文将通过示例对两者进行比较,并分析它们的适用场景与注意事项。

隐式动画

隐式动画是通过修饰器(如 .animation.transaction)声明在状态变化时应应用的动画效果。它能沿视图树向下传递,并且可以被覆盖。

Swift
struct ImplicitAnimationDemo: View {
    @State private var isActive = false
    var body: some View {
        VStack {
            VStack {
                Text("Hello")
                    .font(.largeTitle)
                    .offset(x: isActive ? 200 : 0)
                    // isActive 变化时, Text("Hello") 的 offset 应用 .smooth 动画
                    .animation(.smooth, value: isActive)

                Text("World")
                    .font(.largeTitle)
                    .offset(x: isActive ? 200 : 0)
            }
            // isActive 变化时,VStack(包含其中的元素)应用 .linear
            .animation(.linear.speed(3), value: isActive)

            // 没有设置动画
            Text("No Animation")
                .offset(x: isActive ? 200 : 0)

            Toggle("Active", isOn: $isActive)
                .padding()
        }
    }
}

执行上面的代码,由于没有给 “No Animation” 设置动画,因此它在平移时没有动画。“World” 将使用在 VStack 上设置的动画 .linear.speed(3),“Hello” 则使用动画 .smooth

显式动画

显式动画使用命令式方法,通过withAnimationwithTransaction 显式定义动画范围。它适用于需要对状态变化动画进行集中控制的场景。

Swift
struct ExplicitAnimationDemo: View {
    @State private var isActive = false
    var body: some View {
        VStack {
            VStack {
                Text("Hello")
                    .font(.largeTitle)
                    .offset(x: isActive ? 200 : 0)
                    // isActive 变化时, Text("Hello") 的 offset 应用 .smooth 动画
                    .animation(.smooth, value: isActive)

                Text("World")
                    .font(.largeTitle)
                    .offset(x: isActive ? 200 : 0)
            }
            // isActive 变化时,VStack(包含其中的元素)应用 .linear
            .animation(.linear.speed(0.5), value: isActive)

            // 没有设置隐式动画
            Text("No Animation")
                .offset(x: isActive ? 200 : 0)
            
            Button("Toggle") {
                withAnimation(.spring){
                    isActive.toggle()
                }
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

执行上面的代码,“No Animation” 由于同样受 isActive 状态变化(包含在显式动画闭包中)的影响,而且没有被声明隐式动画,此时它将使用 withAnimation 提供的动画设定:.spring

两者对比

  • 互补关系:隐式动画和显式动画是互补的机制,分别适用于不同的动画需求场景。
  • 精细调整与集中控制:隐式动画通过对具体元素的声明,实现对动画细节的精准控制;显式动画则更适合需要集中管理状态变化动画的场景。
  • 默认优先级:显式动画可为未设置隐式动画的视图提供默认动画效果,保证一致性。
  • 动画屏蔽规则:当显式动画通过 disablesAnimations 设置为 true 时,隐式动画将被屏蔽而失效。
Swift
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction){
    isActive.toggle()
}

隐式动画的注意事项

随着 SwiftUI 的不断更新,新增了多种隐式动画声明方式。在可能的情况下,建议优先使用新版本的隐式动画设置,以便为特定元素提供更精确的动画控制和指引。

  • .animation(.spring) 对当前分支的所有状态变化应用 .spring 动画,无论具体状态为何。从 iOS 14 后废弃。
  • .animation(.spring, value: isActive) 仅在 isActive 状态发生变化时,对当前分支应用 .spring 动画。
  • .animation(animation:, body:) 仅为特定元素声明动画,而非影响整个分支。

延伸阅读