首页 > 解决方案 > SwiftUI 结合观察更新

问题描述

我有一个带有支持 ViewModel 的 SwiftUI 表单。我希望在 ViewModel 更改时启用保存按钮。我有以下代码:

class ViewModel: ObservableObject {
    @Published var didUpdate = false
    @Published var name = "Qui-Gon Jinn"
    @Published var color = "green"
    private var cancellables: [AnyCancellable] = []

    init() {
        self.name.publisher.combineLatest(self.color.publisher)
            .sink { _ in
                NSLog("Here")
                self.didUpdate = true
            }
            .store(in: &self.cancellables)
    }
}

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        NavigationView {
            Form {
                Toggle(isOn: self.$viewModel.didUpdate) {
                    Text("Did update:")
                }
                TextField("Enter name", text: self.$viewModel.name)
                TextField("Lightsaber color", text: self.$viewModel.color)
            }
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .navigationBarItems(
                trailing:
                Button("Save") { NSLog("Saving!") }
                    .disabled(!self.viewModel.didUpdate)
            )
        }
    }
}

这段代码有两个问题。

第一个问题是,在 ViewModel 实例化时,日志将显示“Here”,因此设置didUpdate为 true。第二个问题是,当用户通过文本字段更改视图模型时,它实际上并没有触发发布者。

这些问题应该如何解决?

(我曾考虑添加didSet{}到 ViewModel 中的每个属性,但是当有很多属性时,这是非常难看的。我也想过向文本字段添加修饰符,但我更喜欢将这段代码放在 ViewModel 中,因为网络更新也可能改变 ViewModel)。

标签: swiftuicombine

解决方案


有一种更简单的方法可以做你想做的事,但是这个选项将来可能不是你想要的。但这一切都归结为状态的可变性

首先,您似乎将 与 混淆ModelViewModel。在您的情况下,模型应该是这样的:

struct Model: Equatable {
    var name = "Qui-Gon Jinn"
    var color = "green"
}

请注意,您的模型是Equatable. 在 swift 中,将为您合成的默认实现只是检查所有元素是否彼此相等,即默认实现看起来像这样:

static func ==(lhs: Model, rhs: Model) -> Bool {
    lhs.name == rhs.name && lhs.color == rhs.color
}

我们可以使用这种行为来获得想要的结果:

struct ContentView: View {
    
    var original: Model
    @State var updated: Model
    
    init(original: Model) {
        self.original = original
        self.updated = original
    }
    
    var body: some View {
            NavigationView {
                Form {
                    TextField("Enter name", text: $updated.name)
                    TextField("Lightsaber color", text: $updated.color)
                }
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .navigationBarItems(
                    trailing:
                    Button("Save") { NSLog("Saving!") }
                        .disabled(original == updated)
                )
            }
        }
}

您现在可以简单地将旧(或新)模型传递给您的ContentView. 每当模型与原始模型不同时,将启用保存按钮,如果相同,则禁用保存。重要提示:这种编写模型的简洁方式只有在您使用 astruct作为模型时才有可能,因为它们具有值语义。出于这个原因,structs在对任务进行建模时,优先于类。

现在,如果您坚持使用您的ViewModel(例如,因为Equatable不可能或效率低下),您可以做类似的事情。然而,首先,请注意这条线

name.publisher

名称上的发布者(可能是 type Publishers.Sequence<String, Never>),而不是@Published值(实际上是 type Published<String>.Publisher)前者发布字符串的每个字符,即这个

let name = "Qui-Gon Jinn"

let cancel = name.publisher.print().sink { _ in }

印刷

Q
u
i
-
...

您真正想要的是名称的预计值,它已经是发布者,即

$name.dropFirst().sink { _ in
    NSLog("Here")
    self.didUpdate = true
}

请注意,您需要删除第一个值,因为模型在订阅后立即发布。您还可以将所有这些包装到上述模型中并调用模型的发布者(如果它的属性发生变化,它将在任何时候发布)。


推荐阅读