View — transition 和 animation

transition #

什么是 transition #

SwiftUI 中,transition 是一个视图修饰符,用于定义视图在进入或退出布局时的动画效果。通过 transition,你可以为视图的出现(Appear)和消失(Disappear)设置一些平滑过渡。

关键特点 #

  • transition 本质上是一种与视图生命周期(显示/隐藏)绑定的动画。
  • 它为视图的进入(插入)和退出(删除)提供了动态效果。
  • 常见的过渡效果包括淡入淡出移动缩放、或者自定义效果

1. 基本用法 #

显示和隐藏视图 #

你需要结合 @Statetransition 配合使用,并用 withAnimation 为过渡增加动画效果。

示例:淡入淡出过渡

import SwiftUI

struct ContentView: View {
    @State private var isVisible = false // 控制视图显示和隐藏

    var body: some View {
        VStack {
            Button("Toggle View") {
                withAnimation { // 添加动画
                    isVisible.toggle()
                }
            }

            if isVisible {
                Text("Hello, World!")
                    .padding()
                    .background(Color.blue)
                    .cornerRadius(10)
                    .transition(.opacity) // 使用透明度过渡
            }
        }
    }
}

效果:

  • 点击按钮时:通过透明度的渐变效果(opacity),视图会平滑显现或消失,而不是瞬间改变。

2. 内置过渡效果 #

(1) 常见的过渡类型 #

SwiftUI 提供了多种内置的过渡效果,包括:

过渡类型描述
.opacity透明度渐变,视图通过淡入淡出的透明度动画显示或隐藏。
.scale缩放过渡,视图通过从小到大或从大到小的缩放动画显示或隐藏。
.slide滑动过渡,视图从屏幕边缘滑动进入或退出。
.move(edge:)从某个边缘移动进入或退出(设置边缘参数,如 .leading.trailing.top.bottom)。
.offset()偏移过渡,视图从指定位置渐渐移动到最终位置,或者离开位置时偏移。
.asymmetric()定义不同的进入(插入效果)和退出(删除效果)的过渡。

(2) 使用 opacity 渐变: #

.transition(.opacity)

当视图显示或隐藏时,透明度会平滑地从 01 或从 10


(3) 使用 scale 缩放: #

.transition(.scale)

当视图显示或隐藏时,视图会从 0x0 缩放到原始大小,或者从原始大小缩小到 0x0


(4) 使用 slide 滑动: #

.transition(.slide)

当视图出现时,它会从屏幕边缘滑动到视图内;当消失时,它会滑出屏幕。

示例:滑动过渡效果

if isVisible {
    Text("Hello, SwiftUI!")
        .transition(.slide) // 使用 slide 过渡
}

(5) 使用 move(edge:)#

.transition(.move(edge: .leading))
  • 当视图插入时,从指定边缘(如 .leading 左边缘)移动到当前布局位置。
  • 当视图被移除时,从当前布局位置移到指定边缘。

示例:从顶部移动进入

if isVisible {
    Rectangle()
        .fill(Color.red)
        .frame(width: 200, height: 100)
        .transition(.move(edge: .top)) // 从顶部进入或退出
}

(6) 组合效果: #

当你想将多个过渡效果组合在一起时,可以使用 .combined(_: Transition)

示例:透明度 + 移动组合

.transition(.opacity.combined(with: .move(edge: .bottom)))
  • 视图在出现时从底部移动并逐渐显现;
  • 视图在消失时透明度降低,并且移回底部。

3. 非对称过渡 #

什么是非对称过渡? #

非对称过渡意味着:视图进入时的过渡效果视图退出时的过渡效果不一样。

使用 .asymmetric(insert:remove:)

  • insert 参数定义视图插入时的效果。
  • remove 参数定义视图移除时的效果。

示例:非对称移动和透明化过渡

.transition(.asymmetric(insert: .move(edge: .trailing), // 从右侧进入
                        remove: .opacity))            // 通过透明化消失

完整示例代码:

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

    var body: some View {
        VStack {
            Button("Toggle View") {
                withAnimation {
                    isVisible.toggle()
                }
            }

            if isVisible {
                Text("Non-symmetric Transition")
                    .padding()
                    .background(Color.orange)
                    .cornerRadius(10)
                    .transition(.asymmetric(insert: .slide, remove: .opacity))
            }
        }
    }
}

4. 自定义过渡 #

如何自定义过渡? #

可以通过 .modifier 定义自定义的插入(进入)和移除(退出)效果。

示例:旋转过渡

import SwiftUI

struct RotateTransition: ViewModifier {
    let rotation: Double

    func body(content: Content) -> some View {
        content
            .rotationEffect(.degrees(rotation)) // 应用旋转效果
            .opacity(rotation == 0 ? 1 : 0)    // 控制透明度,消失时透明度为 0
    }
}

extension AnyTransition {
    static var rotate: AnyTransition {
        AnyTransition.modifier(
            active: RotateTransition(rotation: 90), // 退出时的效果
            identity: RotateTransition(rotation: 0) // 初始情况下(静止时)的效果
        )
    }
}

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

    var body: some View {
        VStack {
            Button("Toggle Rotation") {
                withAnimation {
                    isVisible.toggle()
                }
            }

            if isVisible {
                Text("Rotated Text")
                    .font(.title)
                    .padding()
                    .background(Color.green)
                    .cornerRadius(10)
                    .transition(.rotate) // 使用自定义旋转过渡
            }
        }
        .padding()
    }
}

效果:

  • 视图出现时会有旋转效果。
  • 视图消失时旋转退出并逐渐消失。

5. 添加进入和退出动画 #

过渡(transition)更关注的是「进入/退出」时的效果。而动画更关注的是「整个视图变化过程」。你可以同时结合 transitionanimation 为视图带来更复杂的动态行为。

示例:结合动画和过渡 #

import SwiftUI

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

    var body: some View {
        VStack {
            Button("Toggle View") {
                withAnimation(.easeInOut(duration: 0.5)) {
                    isVisible.toggle()
                }
            }

            if isVisible {
                RoundedRectangle(cornerRadius: 20)
                    .fill(Color.blue)
                    .frame(width: 200, height: 100)
                    .transition(.scale) // 使用缩放动画过渡
            }
        }
    }
}

6. 注意事项 #

  1. 只适用于插入/移除的视图:

    • transition 只作用于通过 条件语句(如 if)或 .onAppear/.onDisappear 动态插入的视图。
    • 如果视图一直存在,并只改变其属性值(不插入/移除),使用 animation 而不是 transition
  2. 过渡范围的限制:

    • 过渡动画仅影响从视图「插入或删除」时的行为,而不会影响现有视图的布局调整或样式变化。
  3. 添加容器视图以避免冲突:

    • 如果给多层嵌套的视图同时应用过渡,可能会有不必要的动画冲突。为每个动画的子视图包裹特定容器是个好方法。

7. 总结 #

  • transition 是一个用来定义视图插入或移除时动画过渡的修饰符。
  • 提供了多种内置效果(opacity, slide, move(edge:), scale 等),也支持自定义效果。
  • 搭配 withAnimationif 可视化内容切换
  • 注意在正确的场景下使用 transition,如视图的动态显示或隐藏,否则需要用 animation

Animation #

SwiftUI 中,动画能够为用户界面带来平滑且自然的动态效果,这不仅能提升用户体验,还能增强应用的交互性。了解动画的体系结构、具体实现方式及核心概念,是将其灵活运用于项目的关键。

接下来,我们从 SwiftUI Animation 的体系结构 入手,详细讲解 Animation 的类型、组成以及使用方式。

SwiftUI 中的 Animation 是一个描述性的数据结构,他定义了动画的行为(例如持续时间、缓动函数等),并将其应用在视图变化上。

动画的构成要素 #

  1. 触发动画的条件

    • 通过状态变化(State Change)驱动动画。
    • 通常绑定到 @State 或其他可以触发视图更新的属性。
  2. 动画类型和曲线(Timing Curve)

    • 包括动画的缓动效果(如 easeInOutlinear)、时长以及重复次数等。
  3. 动画修饰符

    • 使用 .animation(_:)withAnimation(_:) 将动画行为应用到视图或者特定代码逻辑。
  4. 组合动画(Chained/Composable Animation)

    • 多个动画行为可以被组合在一起,用于更复杂的效果。

1. Animation 的类型 #

SwiftUI 提供了多种内置动画类型,主要以时间函数(Timing Function)和缓动曲线(Easing)为核心,控制动画的速度、轨迹和效果。

1.1 基础动画类型 #

动画类型描述
linear线性动画,速度恒定,不受缓动影响。
easeIn渐入动画,动画开始时速度慢,随后逐渐加速。
easeOut渐出动画,动画开始时速度快,随后逐渐减速。
easeInOut渐入渐出动画,开始速度慢,随后加速,再渐渐减速,适合自然切换效果。
spring弹性动画,产生弹跳和物理感。

示例:不同类型动画

import SwiftUI

struct BasicAnimationExample: View {
    @State private var isMoved = false

    var body: some View {
        VStack(spacing: 20) {
            Button("Trigger Animation") {
                withAnimation(.easeInOut(duration: 1.5)) {
                    isMoved.toggle()
                }
            }

            // 动画类型:EaseInOut
            RoundedRectangle(cornerRadius: 16)
                .fill(Color.blue)
                .frame(width: 100, height: 100)
                .offset(x: isMoved ? 150 : -150, y: 0)
        }
    }
}

1.2 弹性动画(Spring Animations) #

弹性动画通过模拟物理弹簧的行为带来自然的动感。主要参数包括:

  • response:定义弹簧的响应速度,值越小速度越快。
  • dampingFraction:定义阻尼系数,控制弹跳的幅度(0 是无阻尼,1 是完全阻尼)。
  • blendDuration:定义动画被打断时的过渡平滑时间。

示例:弹性动画效果

import SwiftUI

struct SpringAnimationExample: View {
    @State private var isScaled = false

    var body: some View {
        Circle()
            .fill(Color.red)
            .frame(width: 100, height: 100)
            .scaleEffect(isScaled ? 1.5 : 1) // 放大和缩小
            .onTapGesture {
                withAnimation(.spring(response: 0.5, dampingFraction: 0.4)) {
                    isScaled.toggle()
                }
            }
    }
}

1.3 重复动画(Repeat Animations) #

通过 repeatCountrepeatForever,控制动画的重复次数或无限重复。

示例:无限旋转动画

import SwiftUI

struct RepeatingAnimationExample: View {
    @State private var rotation = 0.0

    var body: some View {
        Image(systemName: "arrow.2.circlepath")
            .font(.largeTitle)
            .rotationEffect(.degrees(rotation))
            .onAppear {
                withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
                    rotation = 360 // 无限旋转
                }
            }
    }
}

属性说明:

  • repeatForever(autoreverses:):是否在每次循环后反向播放。
  • repeatCount(_ count, autoreverses: true):设置具体重复次数。

1.4 自定义动画(Custom Timing Curves) #

SwiftUI 支持用贝塞尔曲线模拟时间函数(Timing Curve),通过 timingCurve 创建独特动画曲线。

示例:自定义贝塞尔曲线动画

import SwiftUI

struct CustomAnimationExample: View {
    @State private var isMoved = false

    var body: some View {
        RoundedRectangle(cornerRadius: 25)
            .frame(width: 100, height: 100)
            .offset(x: isMoved ? 150 : -150)
            .onTapGesture {
                withAnimation(.timingCurve(0.2, 0.8, 0.4, 1.0, duration: 1.5)) {
                    isMoved.toggle()
                }
            }
    }
}

2. Animation 的使用方式 #


2.1 修饰符 .animation(_:value:) #

animation(_:value:) 将动画绑定到视图属性的变化上,当指定的 value 发生变化时,系统会自动触发动画效果。

示例:绑定自动触发动画

struct AnimationBindingExample: View {
    @State private var isScaled = false

    var body: some View {
        Circle()
            .fill(Color.purple)
            .frame(width: isScaled ? 150 : 50, height: isScaled ? 150 : 50)
            .animation(Animation.easeInOut(duration: 1), value: isScaled) // 绑定动画
            .onTapGesture { isScaled.toggle() }
    }
}

2.2 withAnimation 函数 #

withAnimation 明确为某个代码块定义动画,适合对多个属性同时应用动画。

示例:点击缩放和旋转

struct WithAnimationExample: View {
    @State private var isAnimating = false

    var body: some View {
        RoundedRectangle(cornerRadius: 20)
            .fill(Color.orange)
            .frame(width: isAnimating ? 200 : 100, height: isAnimating ? 200 : 100)
            .rotationEffect(.degrees(isAnimating ? 45 : 0))
            .onTapGesture {
                withAnimation(.easeInOut(duration: 1.5)) {
                    isAnimating.toggle()
                }
            }
    }
}

3. 动画的可组合性(Composable Animations) #

SwiftUI 支持对动画进行组合,让多个动画行为在同一时间执行。

3.1 多个动画组合 #

通过不同的动画作用于同一视图的多个属性。

struct ComposableAnimationsExample: View {
    @State private var scale: CGFloat = 1
    @State private var opacity: Double = 1

    var body: some View {
        Circle()
            .scaleEffect(scale)
            .opacity(opacity)
            .onTapGesture {
                withAnimation(.easeInOut(duration: 1)) { scale = 1.5 }
                withAnimation(.linear(duration: 1)) { opacity = 0.5 }
            }
    }
}

3.2 动画时间线 #

为了实现依次执行不同动画,可以通过状态值切换让动画按时间线进行:

struct SequentialAnimationsExample: View {
    @State private var step = 0

    var body: some View {
        VStack {
            Circle()
                .offset(x: step >= 1 ? 100 : 0)
                .scaleEffect(step >= 2 ? 2 : 1)
                .onTapGesture {
                    withAnimation(Animation.easeOut(duration: 0.5)) { step = 1 }
                    withAnimation(Animation.easeInOut(duration: 0.5).delay(0.5)) { step = 2 }
                }
        }
    }
}

4. 动画的注意事项 #

  1. 使用 State 或绑定驱动动画
    动画需要通过可观察的数据发生变化时触发,常见的方式是通过 @State 或绑定到 Binding

  2. 结合视图生命周期(如 onAppear/onDisappear)触发动画
    有些动画可以通过 onAppear 自动启动:

    .onAppear {
        withAnimation { ... }
    }
    
  3. 防止不必要的动画
    如果某些视图变化不需要动画效果,可以通过手动设置 .animation(nil) 禁用动画。

  4. 优选系统内置动画曲线
    系统内置的缓动曲线(如 easeIneaseOut)大多符合用户期待,只有在特殊场景时才使用自定义曲线。


总结 #

SwiftUI 的动画体系简单明了,同时又具备高度灵活性,具有如下特点:

  1. 提供内置的丰富动画类型(lineareaseInspring 等)。
  2. 支持复杂的组合、时间线、顺序和自定义动画。
  3. 尽量通过状态驱动动画,不再需要复杂的手动管理。

可以根据项目需求选择合适的动画类型和方式,灵活运用动画完成精致的动态体验。

过渡与动画对比 #

SwiftUI 中,过渡(Transition)动画(Animation) 是用户界面中常用的两种动效工具。虽然它们都会影响视图的呈现和交互效果,但各自有不同的用途和特性。

下面从核心概念、作用范围、使用场景和注意事项等方面,系统地比较两者的区别。


1. 核心概念 #

动画(Animation) #

动画用来为视图属性的变化提供动态效果,比如平滑移动、缩放、改变颜色或透明度。动画的核心在于让视图的属性(位置、大小、颜色等)在变化过程中平滑过渡。

  • 动画 作用于已有视图,对视图的属性传递时间上变化(例如:位置、透明度、大小)。
  • 动画本身不会插入或删除视图,但可以让现有视图动态响应属性变化。

例子:扩大视图的尺寸

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

    var body: some View {
        RoundedRectangle(cornerRadius: 20)
            .frame(width: isExpanded ? 200 : 100, height: isExpanded ? 200 : 100)
            .foregroundColor(isExpanded ? .blue : .green)
            .onTapGesture {
                withAnimation(.easeInOut) {
                    isExpanded.toggle()
                }
            }
    }
}

在这个例子中:

  • 动画作用于 widthheight 的变化,使新尺寸的设置平滑过渡。
  • 视图的进入/退出不涉及动画,而只是其外观状态发生改变。

过渡(Transition) #

过渡专门用来处理视图的插入(Appear)与移除(Disappear),规定视图在布局中如何进入或离开。过渡的核心功能是控制视图的显示和隐藏表现。

  • 过渡 作用于视图本身的存在与否,定义视图从无到有(或从有到无)的过渡效果。
  • 过渡不会修改视图的属性,而是通过一些动态效果(如透明、滑动、缩放等)控制视图插入/移除时的视觉表现。

例子:视图的淡入/淡出

struct TransitionExample: View {
    @State private var showView = false

    var body: some View {
        VStack {
            Button("Toggle View") {
                withAnimation {
                    showView.toggle()
                }
            }

            if showView {
                RoundedRectangle(cornerRadius: 20)
                    .frame(width: 100, height: 100)
                    .foregroundColor(.blue)
                    .transition(.opacity) // 使用过渡效果
            }
        }
    }
}

在这个例子中:

  • 过渡将决定视图进入或消失时是否通过透明度渐变来实现动态效果。
  • 没有任何视图属性(如位置或大小)被改变,只有其存在状态被管理。

2. 区别对比 #

特性动画(Animation)过渡(Transition)
定义为视图的属性变化提供动态效果。为视图的插入与移除提供动态效果。
作用范围已经存在的视图,对其属性(如大小、透明度、颜色、位置等)施加动画效果。动态管理视图的显示/隐藏过程。
使用时机属性值在视图生命周期中更改(例如点击按钮后改变按钮大小)。视图被加入或移除布局时(例如通过条件判断 if 控制的视图)。
触发条件由视图属性的更改触发动画,例如宽度、位置等状态改变。当视图插入或移除父级布局时触发过渡效果。
实现方式使用 .animation() 修饰符或 withAnimation 包装代码。使用 .transition() 定义进入/移除的方式,通常与 if/else 或显示状态结合使用。
内置效果提供缓动函数(如 .easeInOut.linear)控制动画曲线。提供标准过渡效果(如 .opacity.move.scale),也可以自定义过渡。
是否动态插入视图不处理视图的新建或删除,只处理现有视图属性的动态变化。专注于视图的显示/隐藏(插入/移除)。

3. 使用场景 #

动画适用场景(Animation) #

动画更适合在视图已经存在时,为其内部属性的变化创建流畅的视觉效果:

  • 视图大小的改变(例如点击按钮后,按钮放大)。
  • 视图位置的移动(例如拖拽后视图返回到原位置)。
  • 控件的颜色切换(例如切换按钮背景颜色)。
  • 圆角、旋转角度、透明度等属性变化动画。

示例:旋转图片 #

struct AnimationExample: View {
    @State private var rotation: Double = 0

    var body: some View {
        Image(systemName: "arrow.right")
            .rotationEffect(.degrees(rotation)) // 绑定旋转角度
            .onTapGesture {
                withAnimation(.easeInOut) {
                    rotation += 90 // 点击后动画旋转 90 度
                }
            }
    }
}

过渡适用场景(Transition) #

过渡更适合在视图的显示与隐藏时,提供流畅的插入和移除效果:

  • 条件触发的视图插入或移除(例如弹出子菜单,关闭模态框)。
  • 添加列表中的新元素或移除元素。
  • 层级导航或视图堆栈中的视图插入与移除。

示例:显示和隐藏内容 #

struct TransitionExample: View {
    @State private var showText: Bool = false

    var body: some View {
        VStack {
            Button("Toggle") {
                withAnimation { 
                    showText.toggle() // 控制显示/隐藏
                }
            }
            if showText {
                Text("Hello World!")
                    .transition(.slide) // 滑动过渡插入
                    .background(Color.blue.opacity(0.3))
            }
        }
    }
}

4. 动画和过渡的结合使用 #

在一个功能中,通常需要结合动画和过渡来实现更复杂的动态效果:

示例:滑入的视图同时修改透明度 #

struct CombinedExample: View {
    @State private var showBox = false

    var body: some View {
        VStack {
            Button("Toggle View") {
                withAnimation { showBox.toggle() }
            }

            if showBox {
                RoundedRectangle(cornerRadius: 20)
                    .fill(Color.blue)
                    .frame(width: 150, height: 150)
                    .transition(.opacity.combined(with: .slide)) // 淡入 + 滑动过渡
                    .animation(.easeInOut(duration: 0.5)) // 加强动态效果
            }
        }
    }
}

这段代码组合了 transitionanimation

  • 过渡:定义了视图淡入(opacity)和从屏幕边缘滑入(slide)的效果。
  • 动画:增加了过渡的曲线和时间指定。

5. 注意事项和总结 #

动画的注意事项 #

  1. 动画仅作用于 已有视图的属性变化,不能插入或移除视图。
  2. 当动画作用多个视图时,可能需要使用 ZStack 或其他容器来分层管理。

过渡的注意事项 #

  1. 过渡只负责视图的进入与退出,不会改变视图处于存在状态时的行为。
  2. 过渡需要与 withAnimation 搭配使用,否则动画不会生效。
  3. 过渡需要动态内容控制,如 if.remove.

总结 #

动画(Animation)过渡(Transition)
作用对象视图的属性变化视图的插入和移除
触发时机属性变化时触发视图从布局插入或移除时触发
实现方式.animation()withAnimation.transition()
适用场景已有视图的平滑动态效果(如移动、缩放)条件控制的显示/隐藏的动态效果

两者可以协同工作,用于实现复杂的动效,比如内容的动态切换,与动画曲线完美结合。

本文共 5707 字,上次修改于 Jan 8, 2025