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、再次刷新预览,程序正常运行

 

文哥博客(https://wenge365.com)属于文野个人博客,欢迎浏览使用

联系方式:qq:52292959 邮箱:52292959@qq.com

备案号:粤ICP备18108585号 友情链接