更新嵌套模型的更改

Updates changes for nested models

默认情况下,ObservableObject 不会为嵌套的可观察对象发出更改事件。这里是视图模型中的嵌套设置对象,然后由视图观察。

在这个小例子中,menu 没有看到 settingsenable 值)的变化。如何使用 Combine 处理此行为以向上传播更改 ContentView?

换句话说,如何手动将嵌套模型中的更改向上传递到从属视图:也许在两者之间引入 属性 包装器以减少涉及的锅炉镀层?

// Wrapper
@propertyWrapper struct UserDefault<T: Codable> {
    private let key: String
    private let defaultValue: T

    init(_ key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var wrappedValue: T {
        get {
            guard let data = UserDefaults.standard.object(forKey: key) as? Data else {
                return defaultValue
            }
            let value = try? JSONDecoder().decode(T.self, from: data)
            return value ?? defaultValue
        }
        set {
            let data = try? JSONEncoder().encode(newValue)
            UserDefaults.standard.set(data, forKey: key)
        }
    }
}
// Values
final class UserSettings: ObservableObject {
    @UserDefault("itemA", defaultValue: true)
    var itemA: Bool { willSet { objectWillChange.send() } }

    @UserDefault("itemB", defaultValue: true)
    var itemB: Bool { willSet { objectWillChange.send() } }
    
    @UserDefault("itemC", defaultValue: true)
    var itemC: Bool { willSet { objectWillChange.send() } }
}
// Model
struct Language: Identifiable, Hashable {
    var id: String
    var enable: Bool
}

enum Item: Identifiable, Hashable {
    var id: String {
        switch self {
        case .item(let language): return language.id
        }
    }
    case item(Language)
}

// ModelView
class ViewModel: ObservableObject {
    let settings = UserSettings()
    @Published var menu: [Item]
    var cancellables: [AnyCancellable] = []

    init() {
        menu = [.item(Language(id: "a", enable: settings.itemA)),
                .item(Language(id: "b", enable: settings.itemB)),
                .item(Language(id: "c", enable: settings.itemC))]
    
        settings.objectWillChange.sink { [unowned self] in
            self.objectWillChange.send()
        }
        .store(in: &cancellables)
    }
}
// View
struct ContentView: View {
    @StateObject var model = ViewModel()

    var body: some View {
        HStack() {
            Button("toggle a \(model.settings.itemA.description)") { model.settings.itemA.toggle() }
            Button("toggle b \(model.settings.itemB.description)") { model.settings.itemB.toggle() }
            Button("toggle c \(model.settings.itemC.description)") { model.settings.itemC.toggle() }
        }
        Menu {
            ForEach(model.menu, id:\.self) { content in //// model.menu is not updated
                switch content {
                case let .item(language):
                    if language.enable { // the value doesn't update
                        Button("Item \(language.id)", action: {
                            print(language.id)
                        })
                    }
                }
            }
        } label: { Text("menu") }
    }
}

在您当前的代码中,menu 仅设置一次(在 init 上)。您想在每次对象更新时设置 menu

为此,请在 menu settings 对象更改其中一个值后更改。这里我们在 main 线程上接收,然后用 setMenu().

更新菜单

您还可以将 UserSettings 更改为使用 didSet 而不是 willSet,然后您将不再需要 .receive(on: DispatchQueue.main)。然而,这可能有点违反直觉objectWillChange.

的意思

代码:

class ViewModel: ObservableObject {
    let settings = UserSettings()
    @Published private(set) var menu: [Item] = []
    private var cancellables: [AnyCancellable] = []

    init() {
        setMenu()

        settings.objectWillChange.receive(on: DispatchQueue.main).sink { [unowned self] in
            setMenu()
        }
        .store(in: &cancellables)
    }

    private func setMenu() {
        menu = [
            .item(Language(id: "a", enable: settings.itemA)),
            .item(Language(id: "b", enable: settings.itemB)),
            .item(Language(id: "c", enable: settings.itemC))
        ]
    }
}

我确实稍微更改了一些不相关的内容,例如访问级别。您不希望用户不小心直接编辑 menu