首先来一段英文,这是apple wwdc中的一句,没兴趣的话可以略过不看
value types do not have a canonical reference that SwiftUI can use as a persistent identity for its views
"值类型没有规范引用,SwiftUI无法将其用作视图的持久标识"-果然每个字都认识,不知道在说啥
规范引用(Canonical Reference):指的是一个唯一的、持久的引用,比如对象在内存中的地址。对于引用类型(类),每个实例在内存中都有唯一的地址,因此可以将其作为标识。这里说的引用是指索引,标记,使用来找到对应值的东西,并非我们经常提到的"引用类型"中的引用。
SwiftUI 面临的挑战
less
// 初始状态
VStack {
Text("Hello") // 视图1
Button("Click") // 视图2
}
// 更新后状态
VStack {
Button("Clicked") // 这是原来的按钮吗?
Text("Hello") // 这是原来的文本吗?
}
当视图更新时:
-
如果 SwiftUI 无法识别视图是"同一个"
1、会导致状态丢失(如输入框内容清空)
2、动画会中断而不是平滑过渡
3、性能下降(因为要重建整个视图、
我们看下if- else的情况
less
import SwiftUI
struct ContentView: View {
@State var showFirst = true
@State var counter = 0
var body: some View {
VStack {
Button("切换视图") {
showFirst.toggle()
}
// 示例1: 不设置 id - SwiftUI 会认为是两个不同的 view
Text("示例1: 不设置 id")
if showFirst {
CounterView(title: "第一个", counter: $counter)
.background(Color.red.opacity(0.3))
} else {
CounterView(title: "第二个", counter: $counter)
.background(Color.blue.opacity(0.3))
}
Divider()
// 示例2: 设置相同的 id - SwiftUI 会认为是同一个 view
Text("示例2: 设置相同的 id")
if showFirst {
CounterView(title: "第一个", counter: $counter)
.background(Color.red.opacity(0.3))
.id("same_view")
} else {
CounterView(title: "第二个", counter: $counter)
.background(Color.blue.opacity(0.3))
.id("same_view")
}
}
.padding()
}
}
struct CounterView: View {
let title: String
@Binding var counter: Int
@State private var internalCounter = 0
var body: some View {
VStack {
Text(title)
Text("内部计数器: \(internalCounter)")
Button("内部 +1") {
internalCounter += 1
}
Text("外部计数器: \(counter)")
Button("外部 +1") {
counter += 1
}
}
.padding()
.border(Color.gray)
.onAppear {
print("创建子视图")
}
}
}
可以发现只要是if-else的swiftUI都会使用结构化标识,不论是否设置id,if和else中的view设置id相同与否,都不影响结果:每一次if和else的转换都会导致生产新的CounterView实例,因为其内部计数都是从0开始。
结论:我们无法在if和else中通过设置view的id来达到优化为只创建一个view的效果对于使用结构化标识的view,swiftUI还会自动优化,将上述例子中的一些代码做一下修改:
css
var body: some View {
VStack {
Button("切换视图") {
showFirst.toggle()
}
//第一种
// showFirst ? CounterView(title: "第一个", counter: $counter)
// .background(Color.red.opacity(0.3)) : CounterView(title: "第二个", counter: $counter)
// .background(Color.blue.opacity(0.3))
//第二种
showFirst ? CounterView(title: "第一个")
.background(Color.red.opacity(0.3)) : CounterView(title: "第二个")
.background(Color.blue.opacity(0.3))
}
}
可以发现无论是第一种方式-使用了绑定,还是第二种-无任何绑定,counterView都只生成了一次,无论我们切换试图多少次,已经生成的counterView的内部计数都是连续的如果我们给每个CounterView的实例设置了不同的id,就可以生成两个view,并且每次切换showFirst,都会重新生成要展示的view可以看出给view设置不同的id,会分割生命周期,这也会导致不显示的view的回收
SwiftUI 的两种身份标识
**结构化标识(位置标识)
通过视图在视图树中的位置识别:
scss
- ContentView
- VStack
- Text("标题") // 位置0
- CounterButton() // 位置1 ← 靠这个位置识别
显式标识(唯一ID)
手动指定唯一标识符:
scss
CounterButton()
.id("mainCounter") // 显式标识