比如当一个Button点击时,触发显式动画
Button("Guess") {
withAnimation(Animation.easeInOut(duration: 3)) {
game.attemptGuess()
selection = 0
}
}利用扩展 扩展Animation
Button("Guess") {
withAnimation(Animation.guess) {
game.attemptGuess()
selection = 0
}
}
extension Animation {
static let guess = Animation.easeInOut(duration: 3)
}为指定内容不使用动画
PegView()
.padding(Selection.border)
.background {
if selection == index, code == .guess {
Selection.shape
.foregroundStyle(Selection.color)
}
}
.overlay { // 遮罩
Selection.shape
.foregroundStyle(Code.isHidden ? Color.gray : .clear)
.animation(nil, value: code.isHidden)
}这样隐藏动画,会使得双向生效,也就是 code.isHidden 从 false 到 true,从 true 到 false 的动画都没了。
PegView()
.padding(Selection.border)
.background {
if selection == index, code == .guess {
Selection.shape
.foregroundStyle(Selection.color)
}
}
.overlay { // 遮罩
Selection.shape
.foregroundStyle(Code.isHidden ? Color.gray : .clear)
.transaction { transaction in
if code.isHidden {
transaction.animation = nil
}
}
}Swift 这种MVVM,声明式组件,其实写法非常像 React.js
var body: some View {
VStack {
Button("Restart")
ScrollView {
// ...
}
if !game.isOver {
PegChooser() // 根据 game.isOver 是否显示View
}
}
}过渡动画,其实就是从有到无、从无到有
if !game.isOver {
PegChooser()
.transition(AnyTransition.offset(x: 0, y: 200))
}当游戏isOver,PegChooser应该消失,向屏幕下方移动,PegChooser从消失到显示就会是从屏幕下方向上移,一正一逆。
坐标系屏幕左上是原点,向右为x正轴,向下为y正轴。
合理利用扩展
if !game.isOver {
PegChooser()
.transition(AnyTransition.pegChooser)
}
extension AnyTransition {
static let pegChooser = AnyTransition.offset(x: 0, y: 200)
}在Button文字Title左边显示图标
Button("Restart", systemImage: "arrow.circlepath", action: restart)
// .labelStyle(iconOnly)
// .labelStyle(.titleOnly)@State private var big: Bool = false
MyView(big: $big.animation(...))可以理解成
Binding(
get: {
big
},
set: { newValue in
withAnimation(.easeInOut) {
big = newValue
}
}
)$big.animation(.easeInOut)
// 返回仍是
Binding<Bool>
// 只是包装了一层动画行为。struct ParentView: View {
@State private var big = false
var body: some View {
MyView(big: $big.animation(.easeInOut))
}
}
struct MyView: View {
@Binding var big: Bool
var body: some View {
Rectangle()
.frame(width: big ? 200 : 100,
height: big ? 200 : 100)
.onTapGesture {
big.toggle()
}
}
}big.toggle() 实际变成了
Binding.set(...)
↓
Transaction(animation: .easeInOut)
↓
更新 State
100->200 会自动动画子 View 根本不用写:
withAnimation {
big.toggle()
}Binding 如果没有 .animation()
MyView(big: $big)
big.toggle()
Binding.set()
↓
State 更新
↓
立即刷新
没有动画什么时候适用?
Binding.animation()
最适合父子视图之间的状态传递。例如父视图希望”只要子视图通过这个绑定修改状态,就自动带动画”,而子视图保持纯粹,只负责业务逻辑:
ParentView
└── $big.animation(.spring())
│
▼
ChildView
│
▼
big.toggle() // 自动动画| 方法 | 动画作用对象 | 触发条件 |
|---|---|---|
withAnimation {} |
一段状态修改 | 包裹的代码执行时 |
$binding.animation() |
Binding 的写操作 |
通过该 Binding 设置新值时 |
.animation(_, value:) |
View |
指定的值变化导致视图更新时 |
如果你有 React 的经验,可以把 SwiftUI 的 Group 当成 比
React Fragment(<>...</>) 功能更多一点的
Fragment。
它是真正的一个 View 类型。Group 不负责布局。
// 作用只有一个:避免增加额外的 DOM 节点。
<>
<Text>Hello</Text>
<Button>OK</Button>
</>
// 或
<React.Fragment>
...
</React.Fragment>SwiftUI 的 Group
Group {
Text("Hello")
Button("OK") {}
}
// 它同样不会产生一个真正的 View 容器。
VStack
├── Group
│ ├── Text
│ └── Button
实际布局时更像,Group 自己不会占空间。
VStack
├── Text
└── Button例如,让多个 View 满足返回一个 View
var body: some View {
if isLogin {
Text("Login")
Text("Success") // ❌
} else {
Text("Guest")
}
}
// 利用 Group
var body: some View {
if isLogin {
Group {
Text("Login")
Text("Success")
}
} else {
Text("Guest")
}
}给多个 View 一起加 Modifier
Group {
Text("A")
Text("B")
Text("C")
}
.font(.title)
.foregroundStyle(.red)matchedGeometryEffect 是 SwiftUI 中最强大的动画 API 之一,它可以让两个不同 View 看起来像同一个 View 在移动、变形。
告诉 SwiftUI:这两个 View 是同一个东西,只是在不同地方出现。
然后 SwiftUI 自动帮你计算:
最终生成一个连续动画。
假设一个红方块点击以后跑到下面,没有 matchedGeometryEffect
if expand {
Spacer()
Rectangle()
.fill(.red)
.frame(width: 200, height: 200)
} else {
Rectangle()
.fill(.red)
.frame(width: 80, height: 80)
Spacer()
}动画会变成 旧View消失 新View出现,即使加了
.animation(.easeInOut, value: expand)也只是 Fade Out、Fade In,SwiftUI 认为 这是两个不同的 Rectangle
使用 matchedGeometryEffect
// 首先创建一个Namespace
@Namespace private var animation
// 然后两个View
if expand {
Spacer()
Rectangle()
.matchedGeometryEffect(id: "box", in: animation)
.frame(width: 200, height: 200)
} else {
Rectangle()
.matchedGeometryEffect(id: "box", in: animation)
.frame(width: 80, height: 80)
Spacer()
}
// 再加上动画
.onTapGesture {
withAnimation(.spring()) {
expand.toggle()
}
}不是两个Rectangle,而是 同一个 Rectangle,位置变化 大小变化 一起动画。
为什么需要 Namespace,Namespace就像 动画世界 或者 说动画组,SwiftUI会去这个Namespace里面找: 有没有另一个id一样的View
.matchedGeometryEffect 完整写法
.matchedGeometryEffect(
id: "box",
in: animation,
properties: .frame
)
// properties 参数默认 .frame
// 也可以是 只动画位置 .position 大小不会变
// 或 .size 只改变大小 位置保持不动
// .frame 位置大小一起变