-
Notifications
You must be signed in to change notification settings - Fork 317
Animating Views and Transitions

使用
SwiftUI时,无论用作何处,我们都可以单独为视图添加动画,或者对视图的状态添加动画。SwiftUI为我们处理所有动画的组合、重叠和中断的复杂性。在本文中,我们会给包含图表的视图设置动画,跟踪用户在使用
Landmarksapp 时行为。我们会看到通过使用animation(_:)修饰符为视图设置动画是多么简单。下载项目文件并按照以下步骤操作,也可以打开已完成的项目自行浏览代码。
- 预计完成时间:20 分钟
- 项目文件:下载
当我们在一个视图上使用 animation(_:) 修饰符时, SwiftUI 会动态的修改这个视图的可动画属性。一个视图的颜色、透明度、旋转、大小以及其他属性都是可动画的。

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(.easeInOut())
//
}
}
if showDetail {
HikeDetail(hike: hike)
}
}
}
}
1.3 添加一个在图表显示时让按钮变大的动画。
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()
.animation(.easeInOut())
}
}
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)
}
}
}
}
1.5 试着在 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)
}
}
}
}
现在我们已经学会如何给单个视图添加动画,是时候给状态的值的改变添加动画了。
这一节,我们会给用户点击按钮并切换 showDetail 状态属性时发生的所有更改添加动画。

2.1 将 showDetail.toggle() 的调用包装到 withAnimation 函数中。
受 showDetail 属性影响的按钮和 HikeDetail 视图现在就都有了动画过渡。
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(.easeInOut(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 尝试在动画期间打开和关闭图表视图。

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)
}
}
}
}
默认情况下,视图通过淡入和淡出过渡到屏幕上和屏幕外。我们可以使用 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:) 修饰符来给视图显示和消失时提供不同的转场。
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)
}
}
}
}
单击条形下方的按钮时,图形会在三组不同的数据之间切换。在本节中,我们将使用组合动画为构成图形的胶囊提供动态、波动的转场。

4.1 把 showDetail 的默认值改成 true ,并把 HikeView 的预览固定在画布中。
这让我们在其他文件中制作动画时依然能在上下文中看到图表。

4.2 在 GraphCapsule.swift 中,添加一个新的计算动画属性,并将其应用于胶囊的形状上。
GraphCapsule.swift
import SwiftUI
struct GraphCapsule: View {
var index: Int
var height: CGFloat
var range: Range<Double>
var overallRange: Range<Double>
var heightRatio: CGFloat {
max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
}
var offsetRatio: CGFloat {
CGFloat((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: CGFloat
var range: Range<Double>
var overallRange: Range<Double>
var heightRatio: CGFloat {
max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
}
var offsetRatio: CGFloat {
CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
}
var animation: Animation {
//
Animation.spring(dampingFraction: 0.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: CGFloat
var range: Range<Double>
var overallRange: Range<Double>
var heightRatio: CGFloat {
max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
}
var offsetRatio: CGFloat {
CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
}
var animation: Animation {
Animation.spring(dampingFraction: 0.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 根据 胶囊在图表上的位置为每个动画添加延迟。
GraphCapsule.swift
import SwiftUI
struct GraphCapsule: View {
var index: Int
var height: CGFloat
var range: Range<Double>
var overallRange: Range<Double>
var heightRatio: CGFloat {
max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
}
var offsetRatio: CGFloat {
CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
}
var animation: Animation {
Animation.spring(dampingFraction: 0.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 观察自定义动画在图表之间转场时是如何营造波动效果的。

SwiftUI 纲要 - 绘制与动画 - App 设计与布局 - 框架集成