SwiftUI Observation 嵌套使用
所属分类:ios | 发布于 2025-01-17
有个被@Observable宏定义的公共类,这个类的属性是存储在UserDeaults中,而且这个类是其它@Observable宏定义的类的一个属性,这算是Observation的嵌套使用,当然,这个需求比简单的Observation嵌套使用更复杂一点。
先从从简单的Observation嵌套使用说起
1、定义一个@Observable的公共类NestedStorage
@Observable
final class NestedStorage {
var storageName: String = "storageName"
}
这个类的storageName可能存储在UserDefaults中,但那毕竟负责,我们先从简单的看起。
2、定义一个@Observable的NestedModel
@Observable
final class NestedModel {
var name: String = "modelName"
var storage: NestedStorage = NestedStorage()
}
这个类有一个属性storage是NestedStorage,这就形成了Observation的嵌套,在iOS17之前使用ObservableObject解决这类问题比较麻烦,但是在iOS17中,使用Observation框架就非常简单,可以直接使用。
3、构建一个NestedObservationView用来展示View
struct MeetNestedObservationView: View {
@State private var model = NestedModel()
var body: some View {
Text("model name: \(model.name)")
Text("storage name: \(model.storage.storageName)")
HStack {
Button("ChangeModelName", action: changeModelName).buttonStyle(.borderedProminent)
Button("ChangeStorageName", action: changeStorageName).buttonStyle(.borderedProminent)
}
}
func changeModelName() {
model.name = "newModelName \(Int.random(in: 0...1000))"
}
func changeStorageName() {
model.storage.storageName = "newStorageName \(Int.random(in: 0...1000))"
}
}
4、预览,看到程序正常运行
在View中传递嵌套的Observation
5、增加一个二级页面MeetNestedSheetView
struct MeetNestedSheetView: View {
@State var model: NestedModel
var body: some View {
Text("model name: \(model.name)")
Text("storage name in model: \(model.storage.storageName)")
HStack {
Button("ChangeModelName", action: changeModelName)
Button("ChangeStorageName", action: changeStorageName)
}
}
func changeModelName() {
model.name = "newModelName IN Sheet \(Int.random(in: 0...1000))"
}
func changeStorageName() {
model.storage.storageName = "newModelStorageName IN Sheet \(Int.random(in: 0...1000))"
}
}
6、修改MeetNestedObservationView,增加弹出Sheet功能
struct MeetNestedObservationView: View {
...
@State private var sheetIsPresented: Bool = false
...
var body: some View {
...
Button("Open Sheet") {
sheetIsPresented.toggle()
}
.sheet(isPresented: $sheetIsPresented) {
MeetNestedSheetView(model: model)
}
...
}
}
7、预览,程序正常运行
Observation搭配UserDefaults使用
8、修改NestedStorage的storageName,使用UserDefaults存储其值
@Observable
final class NestedStorage {
// var storageName: String = "storageName"
var storageName: String {
get {
if let name = UserDefaults.standard.string(forKey: "storageNameKey") {
return name
} else {
return "defaultStorageName"
}
}
set {
UserDefaults.standard.set(newValue, forKey: "storageNameKey")
}
}
}
9、预览,发现页面不能按预想规则刷新
10、手动刷新预览,发现存储的值已经改变
此时,我们得到结论,当ChangeStorageName按钮被点击后,NestedStorage的storageName的存储在UserDefaults中的值已经改变,但是并没有响应到View。
11、重构get()、set()解决响应到视图的问题
@Observable
final class NestedStorage {
// var storageName: String = "storageName"
@ObservationIgnored
var storageName: String {
get {
access(keyPath: \.storageName)
if let name = UserDefaults.standard.string(forKey: "storageNameKey") {
return name
} else {
return "defaultStorageName"
}
}
set {
withMutation(keyPath: \.storageName) {
UserDefaults.standard.set(newValue, forKey: "storageNameKey")
}
}
}
}
这种实现的基本逻辑是:在 get 方法中通过 access 注册观察者并从 UserDefaults 获取数据,在 set 方法中奖数据保存到UserDefaults 并通过 withMutation 通知观察者数据变化。这和 @Observable 宏生成的代码原理相似,只是将数据存储位置从内部私有变量改成了UserDefaults。
这段解决方法是从肘子的文章中看到的,感谢肘子,原文链接:SwiftUI 中的 UserDefaults 与 Observation:如何实现精准响应
12、重置UserDefaults中的storageName,再次运行程序,程序按预期运行了
共享NestedStorage
期望NestedStorage在多个被@Observable定义的class中共享使用,实际应用中,NestedStorage中可能保存的是用户的登录信息,App的购买情况等需要共享的数据。
13、定义一个有NestedStorage属性的@Observable类
@Observable
final class SheetModel {
var name: String = "sheetName"
var storage: NestedStorage = NestedStorage()
}
14、修改MeetNestedSheetView
struct MeetNestedSheetView: View {
@State var model: NestedModel
@State var sheet: SheetModel = SheetModel()
var body: some View {
Text("model name: \(model.name)")
Text("storage name in model: \(model.storage.storageName)")
Text("sheet name: \(sheet.name)")
Text("storage name in sheet: \(sheet.storage.storageName)")
HStack {
Button("ChangeModelName", action: changeModelName)
Button("ChangeStorageName", action: changeStorageName)
}
HStack {
Button("ChangeSheetName", action: changeSheetName)
Button("ChangeSheetStorageName", action: changeSheetStorageName)
}
}
func changeModelName() {
model.name = "newModelName IN Sheet \(Int.random(in: 0...1000))"
}
func changeStorageName() {
model.storage.storageName = "newModelStorageName IN Sheet \(Int.random(in: 0...1000))"
}
func changeSheetName() {
sheet.name = "newSheetName \(Int.random(in: 0...1000))"
}
func changeSheetStorageName() {
sheet.storage.storageName = "newSheetStorageName IN Sheet \(Int.random(in: 0...1000))"
}
}
15、重置NstorageName后刷新预览运行
运行效果和预期的不一致。在SheetView页面通过sheetModel更改nestedStorage的storageName,该改变不会跨页面响应。
16、要解决共享问题,我们自然而然的想到了单例模式,来吧,开干
@Observable
final class NestedStorage {
static let shared = NestedStorage()
private init() { }
// var storageName: String = "storageName"
var storageName: String {
...
}
}
设置init()方法为private,禁止从外部创建对象,新建一个名叫 shared 的静态变量,这就是Swift的单例模式。
17、修改NestedModel
@Observable
final class NestedModel {
var name: String = "modelName"
// var storage: NestedStorage = NestedStorage()
var storage: NestedStorage = NestedStorage.shared
}
18、修改SheetModel
@Observable
final class SheetModel {
var name: String = "sheetName"
//var storage: NestedStorage = NestedStorage()
var storage: NestedStorage = NestedStorage.shared
}
19、再次刷新预览,程序正常运行