创建 @State
包裹属性,可以理解为一个对象包含的所有数据,当任一值发生变化时都会更新 UI。其实际的情况是,每次结构体中的值更改时,整个结构体都会更改,就像每次键入名字或姓氏的键时都会出现一个新用户一样。这听起来可能很浪费,但实际上速度非常快。
//如以下例子:
struct User {
var firstName = "Bilbo"
var lastName = "Baggins"
}
struct ContentView: View {
@State private var user = User()
var body: some View {
VStack {
Text("Your name is \\(user.firstName) \\(user.lastName).")
TextField("First name", text: $user.firstName)
TextField("Last name", text: $user.lastName)
}
}
}
mutating
关键字,因为您可以更改常量类的属性。这意味着:
//于是将以上代码改成:这时发现代码不生效了,视图无法根据 类的属性值 的变化而进行更新了
class User {
var firstName = "Bilbo"
var lastName = "Baggins"
}
struct ContentView: View {
@State private var user = User()
...
}
因为当使用 @State
时,我们要求 SwiftUI 监视属性的更改。如果属性已更改,SwiftUI 将重新调用视图的 body
属性(重新计算);所以当 User
是结构体时,每次修改该结构体的属性,Swift 实际上都是创建了该结构体的新实例。 @State
能够发现该更改,并自动重新加载视图。
而现在有了一个类,这种行为就不再发生了。因为 Swift 可以直接修改值。还记得我们如何必须对修改属性的“结构体”方法使用 mutating
关键字吗?这是因为,如果我们将结构体的属性创建为变量,但结构体本身是常量,则我们无法更改属性。Swift 需要能够在属性更改时销毁并重新创建整个“结构体”,而这对于常量结构体来说是不可能的。类不需要 mutating
关键字,因为即使类实例被标记为常量,Swift 仍然可以修改变量属性。
<aside>
💡 现在 User
是一个类,类的实例储存的是(指针/引用),虽然类内部的属性发生了改变,但是 var user
这个指针和引用关系本身没有改变(即这里的 @State
状态参数没有变化),所以 @State
没有注意到任何东西,也就不能重新加载视图。
虽然类内部的属性值发生更改,但 @State
不会监视这些值,所以视图不会重新加载以反映该更改。
</aside>
这时可以通过一个小改动来解决这个问题:就是在类之前添加行 @Observable
宏
//它应该看起来像这样:
@Observable
class User {
var firstName = "Bilbo"
var lastName = "Baggins"
}
使用 @Observable
的类可以跨多个 SwiftUI 视图中使用,并且当该类的属性发生更改时,所有这些视图都会更新。
@State
与 struct
结构体一起使用,即可实现“当值更改时,SwiftUI 视图自动更新”@State
与 class
一起使用,也获得同样效果,则必须在 class
前面添加 @Observable
<aside>
💡 使用 struct
时, @State
属性包装器使值保持活动状态,并监视它的更改;
使用 class
时, @State
只是为了保持对象处于活动状态,所有对更改的监视和更新视图都由 @Observable
负责。
</aside>
@Observable
class User {
var firstName = "Bilbo"
var lastName = "Baggins"
}
struct ContentView: View {
@State private var user = User()
...
}
如上,这是一个有两个字符串变量的类,它以 @Observable
开头,代表它告诉 SwiftUI 要监视该 class
中每个单独的属性的更改,并在属性发生更改时重新加载依赖于该属性的任何视图。说起来简单,但这里隐藏了大量工作:
@Observable
它是一个宏,是 Swift 悄悄重写代码以添加额外功能的方式import Observation
代码,并且在 @Observable
上右键选择 Expand Macro
,便能看见详细代码@ObservationTracked
,这意味着 Swift 和 SwiftUI 正在监视它们的更改@ObservationTracked
,还可以展开宏(它是宏中的宏)。该宏的作用是跟踪任何属性的读取或写入,以便 SwiftUI 只能更新绝对需要刷新的视图Observable
协议。这很重要,因为 SwiftUI 的某些部分认为这意味着“可以监视此类的更改”参见:3. 编码 @Observable 类(CodingKey)
如果某数据类型的所有属性已经符合 Codable
,那么该类型本身就可以符合 Codable
,无需额外的工作。然而,由于 Swift 重写代码的方式,在编码处理使用了 @Observable
宏的类时,事情会有点棘手。因为编码带 @Observable
宏的类时,程序会悄悄重写该类,以便它可以被 SwiftUI 监控,而这里重写可能会导致各种问题。例如,它会把原本的 "name":"Taylor"
变成 "_name":"Taylor"
,多了个下划线。
//例如:以下代码
@Observable
class User: Codable {
var name = "Taylor"
}
struct ContentView: View {
var body: some View {
Button("Encode Taylor", action: encodeTaylor)
}
func encodeTaylor() {
//编码 @Observable 类
let data = try! JSONEncoder().encode(User())
//解码,打印出来,会发现属性值已经被篡改了
let str = String(decoding: data, as: UTF8.self)
print(str) //打印结果 {"_name":"Taylor","_$observationRegistrar":{}}
}
}
为了解决这个问题,我们需要准确地告诉 Swift 应该如何编码和解码我们的数据。具体做法是:
Observable
类中嵌套定义一个 enum
枚举,该枚举必须符合 string
和 CodingKey
协议;case _name = "name"
;//修改后的代码:
//在枚举内部,需要为要保存的每个属性编写一个case,以及包含要为其指定名称的原始值
//在该例子中,_name 就是编写的case的底层存储,它的值是对应字符串“name”,不带下划线
@Observable
class User: Codable {
//注意:该 enum 必须符合 CodingKey 协议
enum CodingKeys: String, CodingKey {
case _name = "name"
}
var name = "Taylor"
}
就是这样!如果您再次尝试该代码,您将看到 name 属性已正确命名,并且混合中也不再有观察注册器 - 结果更加清晰。