更新嵌套模型的更改
Updates changes for nested models
默认情况下,ObservableObject 不会为嵌套的可观察对象发出更改事件。这里是视图模型中的嵌套设置对象,然后由视图观察。
在这个小例子中,menu
没有看到 settings
(enable
值)的变化。如何使用 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
。
默认情况下,ObservableObject 不会为嵌套的可观察对象发出更改事件。这里是视图模型中的嵌套设置对象,然后由视图观察。
在这个小例子中,menu
没有看到 settings
(enable
值)的变化。如何使用 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
。