SwiftUI中的基础动画和过渡(三)

在之前关于隐式动画显式动画的文章中,我们都是将动画添加给一个已经存在的视图。在 SwiftUI,它允许我们去定义一个视图的出现和移除,这被叫做过渡(Transitions)

默认情况下,SwiftUI 中的视图的出现和移除使用的fade-infade-out过渡效果。除此之外,SwiftUI 也内置了slidemoveopacity等过渡效果。

构建一个简单的过渡效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct ContentView: View {
var body: some View {
VStack {
RoundedRectangle(cornerRadius: 10)
.frame(width: 300, height: 300)
.foregroundStyle(.green)
.overlay {
Text("显示详情")
.font(.system(.largeTitle, design: .rounded))
.bold()
.foregroundStyle(.white)
}

RoundedRectangle(cornerRadius: 10)
.frame(width: 300, height: 300)
.foregroundStyle(.purple)
.overlay {
Text("哇哦,详情信息")
.font(.system(.largeTitle, design: .rounded))
.bold()
.foregroundStyle(.white)
}
}
}
}

在上面的代码中,我们使用VStack布局管理了两个RoundedRectangle。接着,我们先让第二个紫色的RoundedRectangle隐藏,当我们点击第一个绿色的RoundedRectangle时在显示。

这里,我们先来定义一个状态变量,用来控制第二个紫色RoundedRectangle的显示或隐藏。

1
@State var isShow: Bool = false

然后将第二个RoundedRectangle使用if语句进行包裹:

1
2
3
4
5
6
7
8
9
10
11
if isShow {
RoundedRectangle(cornerRadius: 10)
.frame(width: 300, height: 300)
.foregroundStyle(.purple)
.overlay {
Text("哇哦,详情信息")
.font(.system(.largeTitle, design: .rounded))
.bold()
.foregroundStyle(.white)
}
}

此时,因为isShow值为false,所以第二个RoundedRectangle是不会显示的。

接着,我们给绿色的RoundedRectangle添加onTaGesture修饰器去识别点击手势,当点击手势发生后,对isShow的状态值为取反,并使用withAnimation添加一个默认的过渡动画。

1
2
3
4
5
.onTapGesture {
withAnimation {
isShow.toggle()
}
}

默认的过渡动画如下:

如果想要改变过渡的动画效果,可以给第二个紫色的RoundedRectangle添加transition修饰器,这个修饰器需要一个动画效果的参数,例如:

1
.transition(.scale(scale: 0, anchor: .center))

scale是一个缩放的效果,它需要两个参数,一个是开始时的比例;另一个是缩放开始的锚点。除了scale效果,还有slideoffsetmoveopaque等。

组合过渡效果

如果我们想要要视图呈现多个过渡效果,可以使用combined方法来组合多个动画效果,例如

1
.transition(.offset(x: -600, y:0).combined(with: .scale(scale: 0, anchor: .leading)))

当然我们也可以组合多个:

1
.transition(.offset(x: -600, y:0).combined(with: .scale(scale: 0, anchor: .leading)).combined(with: .opacity))

定义重复使用的动画

有时候,我们可能想要重复使用一个动画效果。这种情况下,我们可以通用定义一个AnyTransition的扩展来实现,例如:

1
2
3
4
5
extension AnyTransition {
static var offsetScaleOpacity: AnyTransition {
AnyTransition.offset(x: -600, y:0).combined(with: .scale).combined(with: .opacity)
}
}

使用方式:

1
.transition(.offsetScaleOpacity)

动画练习

练习一

使用动画和过渡创建下面的效果:

完整代码和注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
struct ContentView: View {
@State private var processing = false // 是否正在处理中
@State private var completed = false // 是否已处理完成
@State private var loading = false // 是否正在加载中

var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 30)
.frame(width: processing ? 250 : 200, height: 60)
.foregroundStyle(completed ? .red : .green)
// 如果不是在处理过程中
if !processing {
Text("Submit")
.font(.system(.title, design: .rounded))
.bold()
.foregroundStyle(.white)
.transition(.move(edge: .top))
}
// 正在处理并且处于未完成状态
if processing && !completed {
HStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(Color.white, lineWidth: 3)
.frame(width: 20, height: 30)
.rotationEffect(.degrees(loading ? 360 : 0 ))
.animation(.easeInOut.repeatForever(autoreverses: false), value: loading)

Text("Processing")
.font(.system(.title, design: .rounded))
.bold()
.foregroundStyle(.white)
}
.transition(.opacity)
.onAppear {
// 页面出现时调用
startProcessing()
}
}
// 完成后显示
if completed {
Text("Done")
.font(.system(.title, design: .rounded))
.bold()
.foregroundStyle(.white)
.onAppear {
self.endProcessing()
}
}
}
.animation(.spring, value: loading)
.onTapGesture {
if !loading {
processing.toggle()
}
}
}

// 开始处理
private func startProcessing() {
self.loading = true
// 模拟处理过程,4s 后更新为处理完成状态
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
self.completed = true
}
}

// 结束处理
private func endProcessing() {
// 3s 后重置按钮的所有状态
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.processing = false
self.completed = false
self.loading = false
}
}
}