为 SwiftUI 组件添加过渡动画效果

了解如何通过 withAnimation、transition 和 Animation 等 SwiftUI 工具,为视图的状态变化和组件的显示与隐藏添加过渡动画,让用户界面更加自然和动感。

为 SwiftUI 组件添加过渡动画效果

在 iOS 开发中,动画是提升用户体验的重要一环。SwiftUI 通过其简洁的声明式语法,让过渡动画变得更加简单易用。

为什么需要使用过渡动画

在 iOS 应用开发中,经常涉及到 UI 组件的变化与切换,例如以下场景:

  • 视图的显示与隐藏
  • 视图的尺寸调整
  • 视图的位置移动等

如果不添加过渡动画,组件的变化就会显得生硬,就像这样:

使用 SwiftUI 框架,我们只需要几行代码即可添加更加自然的过渡效果:

SwiftUI 中的动画组件

SwiftUI 提供了多个用于实现过渡动画的组件,用于控制视图的状态变化,为视图添加流畅的动画效果。

  • withAnimation 是用来包裹状态变化的函数,确保 SwiftUI 在状态变化时应用动画。它主要用于指定何时应用动画效果,但是不指定具体的动画效果类型。
  • Animation 则定义了具体的动画的类型、节奏和时长,主要用于视图属性变化的动画,例如位置的移动、大小的变化、颜色的改变等。
  • transition 和 Animation 类似,但只作用于视图的显示和隐藏这个过程。

它们协同工作,使得动画体验更加丰富和自然。

在下面的 AnimationView 组件中,我们将逐步添加各种动画效果,以帮助你更好地理解这些功能:

import SwiftUI

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

    var body: some View {
        VStack {
            Button("Toggle Size") {
                isExpanded.toggle()
            }
            Rectangle()
                .frame(
                    width: isExpanded ? 200 : 100,
                    height: isExpanded ? 200 : 100
                )
                .foregroundStyle(Color.blue)
        }
    }
}

// 添加 PreviewProvider 来进行预览
struct AnimationView_Previews: PreviewProvider {
    static var previews: some View {
        AnimationView()
    }
}

不过目前,这个示例还没有任何过渡动画,视图的变化看起来很生硬:

使用 withAnimation 函数添加动画

withAnimation 是一个函数,这个函数的主要目的是让 SwiftUI 在视图的状态发生变化时应用动画效果。它负责告诉 SwiftUI "请为接下来的状态变化应用动画",但它不会决定具体的动画效果。

例如,你可以使用 withAnimation 来包裹 isExpanded 状态的切换:

import SwiftUI

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

    var body: some View {
        VStack {
            Button("Toggle Size") {
                withAnimation() {
                    isExpanded.toggle()
                }
            }
            Rectangle()
                .frame(
                    width: isExpanded ? 200 : 100,
                    height: isExpanded ? 200 : 100
                )
                .foregroundStyle(Color.blue)
        }
    }
}

struct AnimationView_Previews: PreviewProvider {
    static var previews: some View {
        AnimationView()
    }
}

当你使用 withAnimation 时,SwiftUI 会应用一个默认的过渡效果,通常是轻微的淡入淡出或滑动过渡。现在视图的变化更加平滑和自然。

你还可以通过传递不同的 Animation 参数来自定义动画的类型和节奏,接下来我们会进一步讨论这一点。

配合 Animation 对象自定义动画效果

当你使用 withAnimation() 而不传递任何参数时,SwiftUI 会使用默认的动画行为(通常是一个平滑的淡入淡出效果)。如果你修改默认的动画效果,此时就需要使用到 Animation() 。

Animation() 用于创建一个动画对象。这个动画对象描述了动画的时间曲线节奏速度等特性。通过 Animation(),你可以指定动画如何进行,比如是否是匀速变化、是否有弹簧效果,或者动画应该持续多长时间。

使用 Animation 有两种方式,你可以简单的调用它的预设动画类型,也可以自定义(如设置持续时间、延迟、重复等)动画效果。

Animation 预设基础动画

Animation 内置了一些常见的基础动画类型来快速实现简单的动画效果,比如弹簧、匀速、缓慢开始和结束等。

这些预设动画可以通过简单的调用来实现,你不用传递任何的参数即可使用。

withAnimation(Animation.spring) {
    // 动画内容
}

这种方式使用了 SwiftUI 的预设动画,例如 spring(),它会自动生成带有弹簧效果的动画曲线。

其他常见的预设动画还包括:

  • Animation.easeIn: 先缓慢加速后匀速。
  • Animation.easeOut: 先匀速后缓慢减速。
  • Animation.easeInOut: 缓慢加速并缓慢减速。
  • Animation.linear: 匀速动画。

试试为我们之前的代码,应用一个 easeInOut 动画效果:

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

    var body: some View {
        VStack {
            Button("Toggle Size") {
                withAnimation(Animation.easeInOut) {
                    isExpanded.toggle()
                }
            }
            Rectangle()
                .frame(
                    width: isExpanded ? 200 : 100,
                    height: isExpanded ? 200 : 100
                )
                .foregroundStyle(Color.blue)
        }
    }
}

Animation 自定义动画参数

如果你需要更精细的控制(比如动画的时长延迟重复次数等),Animaiton 也允许你基于预设动画进行进一步定制。

例如,你可以通过指定持续时间延迟来自定义动画:

withAnimation(Animation.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0.5)) {
    // 动画内容
}

或者使用 easeInOut 并指定持续时间:

withAnimation(Animation.easeInOut(duration: 2.0)) {
    // 动画内容
}

此外,你还可以为动画添加延迟重复

withAnimation(Animation.linear(duration: 2.0).delay(1.0)) {
    // 动画内容
}

withAnimation(Animation.spring().repeatForever(autoreverses: true)) {
    // 动画内容
}

为示例代码添加重复动画:

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

    var body: some View {
        VStack {
            Button("Toggle Size") {
                withAnimation(Animation.linear(duration: 1.0).repeatForever(autoreverses: true)) {
                    isExpanded.toggle()
                }
            }
            Rectangle()
                .frame(
                    width: isExpanded ? 200 : 100,
                    height: isExpanded ? 200 : 100
                )
                .foregroundStyle(Color.blue)
        }
    }
}

何时使用自定义动画参数

许多开发者常常误以为基础动画过于简单,因此倾向于频繁使用自定义动画效果。事实上,恰恰相反,SwiftUI 内置的基础动画是由 Apple 团队精心设计的,经过广泛应用和市场验证,能够满足大多数场景的需求。除非你非常清楚基础动画无法满足具体需求的原因,并且对如何自定义动画参数有深入理解,否则优先使用内置的基础动画往往是最佳选择

此外,SwiftUI 本身并未提供完全自定义动画曲线的机制(如直接定义贝塞尔曲线),必须基于基础动画进行二次调整。

使用 .transition 修饰器

.transition() 是一个 SwiftUI 修饰器,主要负责视图的出现和消失时的动画效果。例如,当一个视图从不可见状态变为可见,或从可见状态变为不可见时,transition 可以定义这个变化的过渡方式,比如淡入、淡出、滑入、滑出等。

transition 只作用在视图的“出现”和“消失”阶段。

.transition 与 Animation 的区别

.transition 修饰器和 Animation 结构体类似,都是用来定义动画效果,但是动画的触发,都需要搭配 withAnimaiton() 函数来实现。

.transition 和 Animation 比较类似,但是使用场景不一样。

特性 transition animation
作用对象 视图的“出现”和“消失”过渡 视图属性的变化(颜色、大小、位置等)
触发条件 视图从不可见到可见,或从可见到不可见 视图属性(如 frame、color、scale)的变化
应用时机 控制视图进入或退出时的过渡效果 控制已经存在的视图属性变化时的动画效果
使用场景 例如:视图的条件性显示或隐藏(用 ifswitch 例如:视图的尺寸、颜色、位置等属性的变化

添加显示与隐藏过渡动画

我们将实例代码稍作修改,变成控制它的显示和隐藏:

import SwiftUI

struct AnimationView: View {
    @State private var isVisible = false

    var body: some View {
        VStack {
            Button("Toggle Visibility") {
                // 使用 withAnimation 包裹状态变化,触发过渡动画
                withAnimation {
                    isVisible.toggle()
                }
            }

            // 条件语句控制视图的显示与隐藏
            if isVisible {
                Rectangle()
                    .frame(width: 200, height: 200)
                    .foregroundStyle(Color.blue)
            }
        }
    }
}

struct AnimationView_Previews: PreviewProvider {
    static var previews: some View {
        AnimationView()
    }
}

在添加 .transition 之前,是这样的效果:

使用 .transition 添加 slide 效果:

struct AnimationView: View {
    @State private var isVisible = false

    var body: some View {
        VStack {
            Button("Toggle Visibility") {
                // 使用 withAnimation 包裹状态变化,触发过渡动画
                withAnimation {
                    isVisible.toggle()
                }
            }

            // 条件语句控制视图的显示与隐藏
            if isVisible {
                Rectangle()
                    .frame(width: 200, height: 200)
                    .foregroundStyle(Color.blue)
                    .transition(.slide)  // 使用 .slide 过渡效果
            }
        }
    }
}

.transition 内置以下常见过渡效果:

  • .opacity:淡入淡出
  • .slide:滑动
  • .move:从特定方向移动进入或退出
  • .scale:放大或缩小

配合使用 .transitionAnimation

transitionanimation 配合使用,可以同时为视图的出现、消失和属性变化添加动画效果。通常,transition 用于处理视图的增减,而 animation 用于处理已经显示的视图的变化。

import SwiftUI

struct AnimationView: View {
    @State private var isVisible = false

    var body: some View {
        VStack {
            Button("Toggle Visibility") {
                // 使用弹簧动画包裹状态变化,触发过渡动画
                withAnimation(Animation.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0.5)) {
                    isVisible.toggle()
                }
            }

            // 条件语句控制视图的显示与隐藏
            if isVisible {
                Rectangle()
                    .fill(LinearGradient(gradient: Gradient(colors: [.blue, .purple]), startPoint: .top, endPoint: .bottom))
                    .frame(width: 200, height: 200)
                    .transition(
                        AnyTransition.scale(scale: 0.1).combined(with: .move(edge: .trailing))
                    )  // 使用 scale 和 move 过渡效果组合
                    .animation(Animation.easeInOut(duration: 1.0))  // 控制动画节奏
            }
        }
    }
}

struct AnimationView_Previews: PreviewProvider {
    static var previews: some View {
        AnimationView()
    }
}

.animation() 修饰器

withAnimatio() 函数和状态一起工作,这意味着状态发生变化的时候,会立即动画效果,并且执行一次之后立即停止——这在实践中通常用于人工交互触发。

想象一下,假设我们想实现下面这样的效果,使用 withAnimation 你要如何实现呢?

事实上,使用 withAnimation 非常难以实现,因为它是单次触发的。因此,我们需要引入一种新的动画触发方式—— .animation()

💡
从 iOS 15.0 开始,.animation() 修饰器已被弃用,你应该使用animation(_:value:) 代替。

为了实现上述效果,你可以这样使用 .animation() :

import SwiftUI

struct DynamicAnimationView: View {
    @State private var progress: CGFloat = 0.5

    var body: some View {
        VStack {
            Slider(value: $progress, in: 0...1)
                .padding()

            Rectangle()
                .frame(width: progress * 200, height: 100)
                .foregroundColor(.blue)
                .animation(.easeInOut, value: progress) // 自动为 progress 的变化应用动画
        }
        .padding()
    }
}

struct DynamicAnimationView_Previews: PreviewProvider {
    static var previews: some View {
        DynamicAnimationView()
    }
}

.animaiton() 会自动监控 value 的变化,并持续的触发动画效果。因此, withAnimation 是视图驱动,.animation()是数据驱动,这是他们最核心的区别。

.animation() 不仅能用于视图属性与状态绑定的场景(就像上面这样),对于普通的场景,仍然适用。我们可以使用 .animation() 重写下面这个代码实现相同的效果:

import SwiftUI

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

    var body: some View {
        VStack {
            Button("Toggle Size") {
                withAnimation() {
                    isExpanded.toggle()
                }
            }
            Rectangle()
                .frame(
                    width: isExpanded ? 200 : 100,
                    height: isExpanded ? 200 : 100
                )
                .foregroundStyle(Color.blue)
        }
    }
}

struct AnimationView_Previews: PreviewProvider {
    static var previews: some View {
        AnimationView()
    }
}

使用 .animation() 后:

import SwiftUI

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

    var body: some View {
        VStack {
            Button("Toggle Size") {
                isExpanded.toggle()  // 不再需要使用 withAnimation,动画由 animation(_:value:) 自动处理
            }
            Rectangle()
                .frame(
                    width: isExpanded ? 200 : 100,
                    height: isExpanded ? 200 : 100
                )
                .foregroundStyle(Color.blue)
                // 使用 animation(_:value:) 自动为 isExpanded 的变化应用动画
                .animation(.easeInOut(duration: 0.5), value: isExpanded)
        }
    }
}

struct AnimationView_Previews: PreviewProvider {
    static var previews: some View {
        AnimationView()
    }
}

我是廖林,希望这篇关于 SwiftUI 动画的指南能够帮助你在 iOS 开发中打造更加流畅和吸引人的用户体验。如果你喜欢我的内容,欢迎订阅我的博客 Code with Ivens,在这里你将找到更多关于 Swift、SwiftUI 以及 iOS 开发的实用教程与技巧。