1.1 统一应用的动画效果
原文链接: https://developer.apple.com/documentation/swiftui/unifying-your-app-s-animations
1.1 统一应用的动画效果
在 SwiftUI、UIKit 和 AppKit 之间创建一致的界面动画体验。
概述
许多应用会结合使用 SwiftUI、UIKit 和 AppKit 来构建和制作界面动画。在 iOS 18 及更高版本中,你可以在 UIKit 和 AppKit 中使用 SwiftUI 动画。SwiftUI 不仅提供种类丰富的标准动画,还支持自定义动画类型。

SwiftUI、UIKit 和 AppKit 使用不同的底层实现来做动画。同时使用多个框架做动画的应用可能遇到一些问题,例如动画时序不同步或其他不一致,这些问题可能难以排查,并导致不理想的用户体验。使用 SwiftUI 动画为所有这些框架中的界面添加动画,就能在每个平台上创造出更一致、更无缝的体验。
创建 SwiftUI 动画
要在 UIKit 或 AppKit 中创建 SwiftUI 动画,请导入 SwiftUI 并创建一个 SwiftUI Animation。然后,把该动画作为参数传给 UIView 上的 animate(_:changes:completion:) 类方法,或 NSAnimationContext 上的 animate(_:changes:completion:) 类方法。下面的示例比较了如何在 SwiftUI、UIKit 和 AppKit 中使用 SwiftUI Animation 类型创建一个基础的弹簧动画。
SwiftUI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| struct ContentView: View {
@State private var position = CGPoint(x: 200, y: 200)
@State private var frame = CGSize(width: 100, height: 100)
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: frame.width, height: frame.height)
.position(position)
Button("Animate") {
// 使用弹簧动画把视图移动到新位置。
let animation = Animation.spring(duration: 0.8)
withAnimation(animation) {
position = CGPoint(x: 100, y: 100)
}
}
}
}
|
UIKit
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
| import SwiftUI
let position = CGPoint(x: 200, y: 200)
let frame = CGSize(width: 100, height: 100)
let buttonPosition = CGPoint(x: 200, y: 400)
let buttonFrame = CGSize(width: 100, height: 100)
override func viewDidLoad() {
super.viewDidLoad()
let myView = UIView(frame: CGRect(origin: position, size: frame))
myView.backgroundColor = .systemBlue
let myButton = UIButton(primaryAction: UIAction(title: "Animate") { _ in
// 使用弹簧动画把视图移动到新位置。
let animation = SwiftUI.Animation.spring(duration: 0.8)
UIView.animate(animation) {
myView.center = CGPoint(x: 100, y: 100)
}
})
myButton.frame = CGRect(origin: buttonPosition, size: buttonFrame)
view.addSubview(myView)
view.addSubview(myButton)
}
|
AppKit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| import SwiftUI
let myView = NSView(frame: CGRect(origin: CGPoint(x: 50, y: 50),
size: CGSize(width: 50, height: 50)))
override func viewDidLoad() {
super.viewDidLoad()
let myButton = NSButton(title: "Animate", target: self, action: #selector(animateView))
view.addSubview(myButton)
myView.wantsLayer = true
myView.layer?.backgroundColor = NSColor.blue.cgColor
view.addSubview(myView)
}
@objc
func animateView() {
// 使用弹簧动画改变视图的尺寸。
let animation = SwiftUI.Animation.spring(duration: 0.8)
NSAnimationContext.animate(animation) {
myView.frame.size = CGSize(width: 100, height: 100)
}
}
|
在 SwiftUI 动画中使用完成处理器
你可以为这些动画方法提供一个可选的完成处理器,系统会在动画完成后自动调用它。下面的示例展示了一个完成处理器,它会在动画完成时改变视图的背景颜色以作为提示。
SwiftUI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| struct ContentView: View {
@State private var color = Color.blue
@State private var position = CGPoint(x: 200, y: 200)
@State private var frame = CGSize(width: 100, height: 100)
var body: some View {
Rectangle()
.fill(color)
.frame(width: frame.width, height: frame.height)
.position(position)
Button("Animate") {
// 使用平滑的弹簧动画把视图移动到新位置。
withAnimation(.smooth) {
position = CGPoint(x: 100, y: 100)
} completion: {
// 动画完成时,改变视图的颜色。
color = Color.red
}
}
}
}
|
UIKit
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
| import SwiftUI
let position = CGPoint(x: 200, y: 200)
let frame = CGSize(width: 100, height: 100)
let buttonPosition = CGPoint(x: 200, y: 400)
let buttonFrame = CGSize(width: 100, height: 100)
override func viewDidLoad() {
super.viewDidLoad()
let myView = UIView(frame: CGRect(origin: position, size: frame))
myView.backgroundColor = .systemBlue
let myButton = UIButton(primaryAction: UIAction(title: "Animate") { _ in
// 使用平滑的弹簧动画把视图移动到新位置。
UIView.animate(.smooth) {
myView.center = CGPoint(x: 100, y: 100)
} completion: {
// 动画完成时,改变视图的颜色。
myView.backgroundColor = .systemRed
}
})
myButton.frame = CGRect(origin: buttonPosition, size: buttonFrame)
view.addSubview(myView)
view.addSubview(myButton)
}
|
AppKit
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
| import SwiftUI
let myView = NSView(frame: CGRect(origin: CGPoint(x: 50, y: 50),
size: CGSize(width: 50, height: 50)))
override func viewDidLoad() {
super.viewDidLoad()
let myButton = NSButton(title: "Animate", target: self, action: #selector(animateView))
view.addSubview(myButton)
myView.wantsLayer = true
myView.layer?.backgroundColor = NSColor.blue.cgColor
view.addSubview(myView)
}
@objc
func animateView() {
// 使用平滑的弹簧动画改变视图的尺寸。
NSAnimationContext.animate(.smooth) {
self.myView.frame.size = CGSize(width: 100, height: 100)
} completion: {
// 动画完成时,改变视图的颜色。
self.myView.layer?.backgroundColor = NSColor.red.cgColor
}
}
|
重新定位 SwiftUI 动画
与 SwiftUI 视图中的动画类似,你可以平滑地重新定位通过 UIView 上的 animate(_:changes:completion:) 类方法或 NSAnimationContext 上的 animate(_:changes:completion:) 类方法执行的动画。重新定位 SwiftUI 动画时,会使用之前动画的速度,让动画以连续的速度继续前进,从而创造出流畅的动画体验。下面的示例展示了对一个进行中的动画进行重新定位。
SwiftUI
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
| struct ContentView: View {
@State private var position = CGPoint(x: 200, y: 200)
@State private var frame = CGSize(width: 100, height: 100)
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: frame.width, height: frame.height)
.position(position)
Button("Animate") {
Task {
// 开始一个动画,把视图移动到新位置。
withAnimation(.spring(duration: 1.0)) {
position = .zero
}
try await Task.sleep(for: .seconds(0.5))
// 重新定位正在运行的动画,把视图移动到另一个位置。
withAnimation(.spring) {
position = CGPoint(x: 100, y: 400)
}
}
}
}
}
|
UIKit
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
| import SwiftUI
let position = CGPoint(x: 200, y: 200)
let frame = CGSize(width: 100, height: 100)
let buttonPosition = CGPoint(x: 200, y: 400)
let buttonFrame = CGSize(width: 100, height: 100)
override func viewDidLoad() {
super.viewDidLoad()
let myView = UIView(frame: CGRect(origin: position, size: frame))
myView.backgroundColor = .systemBlue
let myButton = UIButton(primaryAction: UIAction(title: "Animate") { _ in
Task {
// 开始一个动画,把视图移动到新位置。
UIView.animate(.spring(duration: 1.0)) {
myView.center = CGPoint(x: 200, y: 200)
}
try await Task.sleep(for: .seconds(0.5))
// 重新定位正在运行的动画,把视图移动到另一个位置。
UIView.animate(.spring) {
myView.center = CGPoint(x: 100, y: 400)
}
}
})
myButton.frame = CGRect(origin: buttonPosition, size: buttonFrame)
view.addSubview(myView)
view.addSubview(myButton)
}
|
AppKit
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
| import SwiftUI
let myView = NSView(frame: CGRect(origin: CGPoint(x: 50, y: 50),
size: CGSize(width: 50, height: 50)))
override func viewDidLoad() {
super.viewDidLoad()
let myButton = NSButton(title: "Animate", target: self, action: #selector(animateView))
view.addSubview(myButton)
myView.wantsLayer = true
myView.layer?.backgroundColor = NSColor.blue.cgColor
view.addSubview(myView)
}
@objc
func animateView() {
Task {
// 开始一个动画,改变视图的尺寸。
NSAnimationContext.animate(.spring(duration: 1)) {
myView.frame.size = CGSize(width: 100, height: 100)
}
try await Task.sleep(for: .seconds(0.5))
// 重新定位正在运行的动画,把视图改为另一个尺寸。
NSAnimationContext.animate(.spring) {
myView.frame.size = CGSize(width: 75, height: 75)
}
}
}
|
排查动画问题
跨框架同步动画时,不同实现之间的行为差异可能会显现出来。在你的应用中排查动画问题时,请记住以下提示:
关于在应用中提供出色动画体验的更多信息,请参阅 增强你的界面动画与过渡效果。