SwiftUI — Bool 和 Animation

在 SwiftUI 中,当你将 Bool 类型的状态更新放在 withAnimation 闭包中时,Bool 的值会立即变化,而动画是随后随着渲染引擎逐帧完成的。这意味着,Bool 的值更改和动画的执行是并行的,即动画开始的时刻值已经更新。


关键行为点 #

  1. Bool 值先立即更新:

    • 无论是在 withAnimation 内更改 Bool 的值,还是状态绑定到其他视图,Bool 的值总是 立刻同步更新,即 UI 知道状态值已经发生变化。
  2. 动画随后执行:

    • Bool 的值更新后,SwiftUI 会触发一个渲染更新,并及时应用动画效果。但动画需要一定的时间(取决于你设置的时长),因此这是一个按帧逐渐变化的视觉效果。

代码示例 #

示例 1:简单的 Bool 切换动画 #

struct BoolWithAnimationExample: View {
    @State private var isToggleOn = false // `Bool` 状态

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 12)
                .fill(isToggleOn ? .green : .red) // 根据`Bool`变量调整颜色
                .frame(width: 100, height: 100)
                .rotationEffect(isToggleOn ? .degrees(360) : .degrees(0)) // 根据状态应用旋转动画

            Button("Toggle") {
                withAnimation(.linear(duration: 1)) {
                    isToggleOn.toggle() // 状态在 `withAnimation` 内更改
                }
            }
        }
    }
}

运行行为:

  1. 点击按钮后,isToggleOn 的状态值立即切换(false -> truetrue -> false)。
  2. 由于状态绑定到视图(颜色和旋转角度),视图的外观会随状态切换触发动画。
  3. 动画以线性模式执行,持续 1 秒,从起始状态过渡到目标状态。

示例 2:观察 Bool 更新时机 #

为了验证是 Bool 值先变化还是动画完成后变化,可以通过打印日志来跟踪状态更新顺序:

struct TrackBoolChangeExample: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 12)
                .fill(.blue)
                .frame(width: isExpanded ? 200 : 100, height: 100)
                .animation(.easeInOut, value: isExpanded) // 添加动画

            Button("Expand/Collapse") {
                withAnimation {
                    print("Before changing: \(isExpanded)") // 打印状态
                    isExpanded.toggle()
                    print("After changing: \(isExpanded)") // 打印状态
                }
            }
        }
    }
}

输出日志(点击按钮执行):

Before changing: false
After changing: true

结果分析:

  • isExpanded 的值会在 toggle() 后立刻改变(即 false -> truetrue -> false),而不取决于动画是否完成。
  • 动画则是视觉上逐帧过渡的效果,开始于状态更新之后。

示例 3:多个状态值影响动画 #

如果多个状态值(不止 Bool)需要影响动画,所有这些值都会立即发生变化,而动画会针对所有已更改的状态渲染过渡。

struct MultiStateWithAnimation: View {
    @State private var isGreen = false
    @State private var isLarge = false

    var body: some View {
        VStack {
            Circle()
                .fill(isGreen ? .green : .red)
                .frame(width: isLarge ? 200 : 100, height: isLarge ? 200 : 100)

            Button("Toggle") {
                withAnimation(.spring()) { // 使用弹簧动画
                    isGreen.toggle() // 立即切换颜色的状态
                    isLarge.toggle() // 同时立即切换尺寸的状态
                }
            }
        }
    }
}

运行行为:

  1. 无论是 isGreen 还是 isLarge,它们的值都会在 toggle() 方法调用时立刻更新。
  2. 动画渲染会基于状态的更改同步发生,弹簧动画(spring)逐渐展现过渡效果。

使用场景提示和注意点 #

  1. 动画与状态更新无冲突:

    • 动画的视觉效果与状态更新 解耦,状态值的更改是即时的,而动画仅负责视觉上的平滑过渡。
    • 不需要担心动画完成后值才会更改。
  2. 动画的是视图属性而非值本身:

    • 动画实际应用在 视图的绑定属性 上(例如 frame, rotationEffect, scaleEffect 等),而 Bool 等状态只是用于驱动这些属性。
  3. 多个绑定值组合时:

    • 动画可以同步处理多个属性的变化,所有参与动画的绑定值都会在动画触发前更新为最新值。
  4. 性能考虑:

    • withAnimation 不适合频繁/快速切换的场景(如快速点击触发),应当结合 Task 或节流逻辑限制状态更新频率。
  5. async 任务结合:

    • 如果你需要等待动画完成后再执行某些逻辑,则需要使用异步工具(如 Task.sleep)手动处理动画等待,因为动画执行和状态变化始终是并行的。

完整结合:如何等待动画完成后执行逻辑? #

默认情况下,SwiftUI 不会在动画完成后触发任何回调。如果你想在动画完成后执行额外逻辑,需要借助 DispatchQueueasync/await

struct WaitForAnimationExample: View {
    @State private var isExpanded = false
    @State private var message = "Waiting..."

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 12)
                .fill(.blue)
                .frame(width: isExpanded ? 200 : 100, height: 100)
                .animation(.easeInOut(duration: 1.0), value: isExpanded)
            
            Text(message) // 显示当前状态信息

            Button("Expand") {
                withAnimation {
                    isExpanded.toggle()
                }
                DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { // 等待动画完成后执行
                    message = "Animation Completed!" // 更新消息
                }
            }
        }
    }
}

总结 #

  • 值的改变(如 Bool先于动画完成立即发生
  • 动画仅控制视觉效果,不干涉变量本身的更新时机。
  • 如果需要等待动画完成后执行后续逻辑,可以通过诸如 DispatchQueue 或异步任务进行延迟处理。
本文共 1432 字,上次修改于 Jan 9, 2025