@Observable 宏

产生背景

创建 @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)
        }
    }
}

类 和 结构体 的重要差异

//于是将以上代码改成:这时发现代码不生效了,视图无法根据 类的属性值 的变化而进行更新了
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 宏使用方法

使用 @Observable 的类可以跨多个 SwiftUI 视图中使用,并且当该类的属性发生更改时,所有这些视图都会更新。

<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 类(CodingKey)

参见: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 应该如何编码和解码我们的数据。具体做法是:

//修改后的代码:
//在枚举内部,需要为要保存的每个属性编写一个case,以及包含要为其指定名称的原始值
//在该例子中,_name 就是编写的case的底层存储,它的值是对应字符串“name”,不带下划线

@Observable
class User: Codable {

		//注意:该 enum 必须符合 CodingKey 协议
    enum CodingKeys: String, CodingKey {
        case _name = "name"
    }
    
    var name = "Taylor"
    
}

就是这样!如果您再次尝试该代码,您将看到 name 属性已正确命名,并且混合中也不再有观察注册器 - 结果更加清晰。