完整中文教程及代码请查看 github.com/WillieWangW…
使用 SwiftUI
时,无论用作何处,我们都可以单独为 view 添加动画,或者对 view 的状态进行动画处理。 SwiftUI
为我们处理所有动画的组合、重叠和中断的复杂性。
在本文中,我们会给包含图表的 view 设置动画,跟踪用户在使用 Landmarks
app 时行为。我们会看到通过使用 animation(_:)
方法为 view 设置动画是多么简单。
下载项目文件并按照以下步骤操作,也可以打开已完成的项目自行浏览代码。
当我们在一个 view 上使用 animation(_:)
方法时, SwiftUI
会动态的修改这个 view 的可动画属性。一个 view 的颜色、透明度、旋转、大小以及其他属性都是可动画的。
1.1 在 HikeView.swift
中,打开实时预览来测试显示和隐藏图表。
确保在本文中过程中都打开了实时预览,这样就可以测试到每一步的结果。
1.2 添加 animation(.basic())
方法来打开按钮的旋转动画。
HikeView.swift
import SwiftUI struct HikeView: View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph(data: hike.observations, path: /.elevation) .frame(width: 50, height: 30) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { self.showDetail.toggle() }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .padding() .animation(.basic()) } } if showDetail { HikeDetail(hike: hike) } } } } 复制代码
1.3 添加一个在图表显示时让按钮变大的动画。
animation(_:)
会作用于 view 所包装的所有可动画的修改。
HikeView.swift
import SwiftUI struct HikeView: View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph(data: hike.observations, path: /.elevation) .frame(width: 50, height: 30) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { self.showDetail.toggle() }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .scaleEffect(showDetail ? 1.5 : 1) .padding() .animation(.basic()) } } if showDetail { HikeDetail(hike: hike) } } } } 复制代码
1.4 把动画类型从 .basic()
改成 .spring()
。
SwiftUI
包含带有预设或自定义缓动的基本动画,以及弹性和流体动画。我们可以调整动画的速度、在动画开始之前设置延迟,或指定动画的重复。
HikeView.swift
import SwiftUI struct HikeView: View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph(data: hike.observations, path: /.elevation) .frame(width: 50, height: 30) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { self.showDetail.toggle() }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .scaleEffect(showDetail ? 1.5 : 1) .padding() .animation(.spring()) } } if showDetail { HikeDetail(hike: hike) } } } } 复制代码
尝试在 scaleEffect
方法上方添加另一个动画方法来关闭旋转动画。
围绕 SwiftUI
尝试结合不同的动画效果,看看都有哪些效果。
HikeView.swift
import SwiftUI struct HikeView: View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph(data: hike.observations, path: /.elevation) .frame(width: 50, height: 30) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { self.showDetail.toggle() }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .animation(nil) .scaleEffect(showDetail ? 1.5 : 1) .padding() .animation(.spring()) } } if showDetail { HikeDetail(hike: hike) } } } } 复制代码
1.6 在继续下一节前,删除两个 animation(_:)
方法。
HikeView.swift
import SwiftUI struct HikeView: View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph(data: hike.observations, path: /.elevation) .frame(width: 50, height: 30) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { self.showDetail.toggle() }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .scaleEffect(showDetail ? 1.5 : 1) .padding() } } if showDetail { HikeDetail(hike: hike) } } } } 复制代码
现在我们已经学会如果给单个 view 添加动画,是时候给状态的值的改变添加动画了。
这一节,我们会给用户点击按钮并切换 showDetail
状态属性时发生的所有更改添加动画。
2.1 将 showDetail.toggle()
的调用包装到 withAnimation
函数中。
受 showDetail
属性影响的公开按钮和 HikeDetail
view 现在就都有了动画过渡。
HikeView.swift
import SwiftUI struct HikeView: View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph(data: hike.observations, path: /.elevation) .frame(width: 50, height: 30) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { withAnimation { self.showDetail.toggle() } }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .scaleEffect(showDetail ? 1.5 : 1) .padding() } } if showDetail { HikeDetail(hike: hike) } } } } 复制代码
减缓动画,看看 SwiftUI
动画是如何可以中断的。
2.2 给 withAnimation
方法传递一个 4 秒的基础动画。
我们可以传递相同类型的动画给 animation(_:)
的 withAnimation
函数。
HikeView.swift
import SwiftUI struct HikeView: View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph(data: hike.observations, path: /.elevation) .frame(width: 50, height: 30) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { withAnimation(.basic(duration: 4)) { self.showDetail.toggle() } }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .scaleEffect(showDetail ? 1.5 : 1) .padding() } } if showDetail { HikeDetail(hike: hike) } } } } 复制代码
2.3 尝试在动画期间打开和关闭图表 view 。
2.4 在进入下一节前,从 withAnimation
函数中移除缓慢动画。
HikeView.swift
import SwiftUI struct HikeView: View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph(data: hike.observations, path: /.elevation) .frame(width: 50, height: 30) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { withAnimation { self.showDetail.toggle() } }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .scaleEffect(showDetail ? 1.5 : 1) .padding() } } if showDetail { HikeDetail(hike: hike) } } } } 复制代码
默认情况下,view 通过淡入和淡出过渡到屏幕上和屏幕外。我们可以使用 transition(_:)
方法来自定义转场。
3.1 给满足条件时显示的 HikeView
添加一个 transition(_:)
方法。
现在图标会滑动显示和消失。
HikeView.swift
import SwiftUI struct HikeView: View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph(data: hike.observations, path: /.elevation) .frame(width: 50, height: 30) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { withAnimation { self.showDetail.toggle() } }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .scaleEffect(showDetail ? 1.5 : 1) .padding() } } if showDetail { HikeDetail(hike: hike) .transition(.slide) } } } } 复制代码
3.2 将转场提取为 AnyTransition
的静态属性。
这可以在您展开自定义转场时保持代码清晰。对于自定义转场,我们可以使用与 SwiftUI
所用相同的 .
符号。
HikeView.swift
import SwiftUI extension AnyTransition { static var moveAndFade: AnyTransition { AnyTransition.slide } } struct HikeView: View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph(data: hike.observations, path: /.elevation) .frame(width: 50, height: 30) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { withAnimation { self.showDetail.toggle() } }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .scaleEffect(showDetail ? 1.5 : 1) .padding() } } if showDetail { HikeDetail(hike: hike) .transition(.moveAndFade) } } } } 复制代码
3.3 换成使用 move(edge:)
转场,这样图表会从同一边滑入和滑出。
HikeView.swift
import SwiftUI extension AnyTransition { static var moveAndFade: AnyTransition { AnyTransition.move(edge: .trailing) } } struct HikeView: View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph(data: hike.observations, path: /.elevation) .frame(width: 50, height: 30) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { withAnimation { self.showDetail.toggle() } }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .scaleEffect(showDetail ? 1.5 : 1) .padding() } } if showDetail { HikeDetail(hike: hike) .transition(.moveAndFade) } } } } 复制代码
3.4 使用 asymmetric(insertion:removal:)
方法来给 view 显示和消失时提供不同的转场。
HikeView.swift
import SwiftUI extension AnyTransition { static var moveAndFade: AnyTransition { let insertion = AnyTransition.move(edge: .trailing) .combined(with: .opacity) let removal = AnyTransition.scale() .combined(with: .opacity) return .asymmetric(insertion: insertion, removal: removal) } } struct HikeView: View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph(data: hike.observations, path: /.elevation) .frame(width: 50, height: 30) VStack(alignment: .leading) { Text(hike.name) .font(.headline) Text(hike.distanceText) } Spacer() Button(action: { withAnimation { self.showDetail.toggle() } }) { Image(systemName: "chevron.right.circle") .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0)) .scaleEffect(showDetail ? 1.5 : 1) .padding() } } if showDetail { HikeDetail(hike: hike) .transition(.moveAndFade) } } } } 复制代码
单击条形下方的按钮时,图形会在三组不同的数据之间切换。在本节中,我们将使用组合动画为构成图形的 Capsule
提供动态、波动的转场。
4.1 把 showDetail
的默认值改成 true
,并把 HikeView
的预览固定在 canvas
中,
这让我们在其他文件中制作动画时依然能在上下文中看到图表。
4.2 在 GraphCapsule.swift
中,添加一个新的计算动画属性,并将其应用于 Capsule
的 shape
。
GraphCapsule.swift
import SwiftUI struct GraphCapsule: View { var index: Int var height: Length var range: Range<Double> var overallRange: Range<Double> var heightRatio: Length { max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15) } var offsetRatio: Length { Length((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange)) } var animation: Animation { Animation.default } var body: some View { Capsule() .fill(Color.gray) .frame(height: height * heightRatio, alignment: .bottom) .offset(x: 0, y: height * -offsetRatio) .animation(animation) ) } } 复制代码
4.3 将动画改为弹性动画,使用初始速度让条形图跳跃。
GraphCapsule.swift
import SwiftUI struct GraphCapsule: View { var index: Int var height: Length var range: Range<Double> var overallRange: Range<Double> var heightRatio: Length { max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15) } var offsetRatio: Length { Length((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange)) } var animation: Animation { Animation.spring(initialVelocity: 5) } var body: some View { Capsule() .fill(Color.gray) .frame(height: height * heightRatio, alignment: .bottom) .offset(x: 0, y: height * -offsetRatio) .animation(animation) ) } } 复制代码
4.4 加快动画速度,缩短每个小节移动到新位置所需的时间。
GraphCapsule.swift
import SwiftUI struct GraphCapsule: View { var index: Int var height: Length var range: Range<Double> var overallRange: Range<Double> var heightRatio: Length { max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15) } var offsetRatio: Length { Length((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange)) } var animation: Animation { Animation.spring(initialVelocity: 5) .speed(2) } var body: some View { Capsule() .fill(Color.gray) .frame(height: height * heightRatio, alignment: .bottom) .offset(x: 0, y: height * -offsetRatio) .animation(animation) ) } } 复制代码
4.5 根据 Capsule
在图表上的位置为每个动画添加延迟。
GraphCapsule.swift
import SwiftUI struct GraphCapsule: View { var index: Int var height: Length var range: Range<Double> var overallRange: Range<Double> var heightRatio: Length { max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15) } var offsetRatio: Length { Length((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange)) } var animation: Animation { Animation.spring(initialVelocity: 5) .speed(2) .delay(0.03 * Double(index)) } var body: some View { Capsule() .fill(Color.gray) .frame(height: height * heightRatio, alignment: .bottom) .offset(x: 0, y: height * -offsetRatio) .animation(animation) ) } } 复制代码
4.6 观察自定义动画在图表之间转场时是如何营造波纹效果的。