SwiftUI 结合观察更新
SwiftUI Combine observing updates
我有一个带有支持 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。第二个问题是,当用户通过文本字段更改视图模型时,它实际上并没有触发发布者。
应该如何解决这些问题?
(我考虑过向 ViewModel 中的每个 属性 添加 didSet{}
但是当有很多属性时这非常难看。我还考虑过向文本字段添加修饰符,但我真的更喜欢将此代码放在 ViewModel 中,因为网络更新也可能会更改 ViewModel)。
有一种更简单的方法可以完成您想要的操作,但是此选项将来可能不是您想要的。但这一切归结为状态的可变性。
首先,您似乎混淆了 Model
和 ViewModel
。在您的情况下,模型应该是这样的:
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
。每当模型与原始模型不同时,保存按钮将被启用,当它相同时,保存被禁用。 重要:只有当您使用 struct
作为模型时,这种编写模型的巧妙方法才有可能,因为它们具有值语义。也正是出于这个原因,在为您的任务建模时 structs
优于 类。
现在,如果您坚持使用您的 ViewModel
(例如,因为无法符合 Equatable
或效率低下),您可以做类似的事情。但是,首先请注意这一行
name.publisher
是 name 上的发布者(类型为 Publishers.Sequence<String, Never>
),而不是 @Published
值(实际上类型为 Published<String>.Publisher
)
前者发布字符串的每个字符,即 this
let name = "Qui-Gon Jinn"
let cancel = name.publisher.print().sink { _ in }
打印
Q
u
i
-
...
您真正想要的是名称的预计值,该名称已经是发布者,即
$name.dropFirst().sink { _ in
NSLog("Here")
self.didUpdate = true
}
请注意,您需要删除第一个值,因为模型会在订阅后立即发布。您还可以将所有这些包装到上述模型中并调用模型的发布者(如果它的属性发生变化,它将发布)。
如果您使用 结构 来保存发件人字段的属性,会更容易。
struct Model {
var name: String
var color: String
}
然后,在self.$model.sink { value in}
中比较新值是否与旧值相同或已更改。
class ViewModel: ObservableObject {
@Published var didUpdate = false
@Published var model: Model
private var cancellables: [AnyCancellable] = []
init() {
self.model = Model(name: "Qui-Gon Jinn", color: "green")
self.$model.sink { value in
guard !(value.name.trimmingCharacters(in: .whitespaces).isEmpty || value.color.trimmingCharacters(in: .whitespaces).isEmpty) else {
self.didUpdate = false
return
}
if value.name != self.model.name {
NSLog("name did chanage")
self.didUpdate = true
}
if value.color != self.model.color {
NSLog("Color did change")
self.didUpdate = true
}
}
.store(in: &self.cancellables)
}
deinit {
self.cancellables.removeAll()
}
}
所有代码
struct Model {
var name: String
var color: String
}
class ViewModel: ObservableObject {
@Published var didUpdate = false
@Published var model: Model
private var cancellables: [AnyCancellable] = []
init() {
self.model = Model(name: "Qui-Gon Jinn", color: "green")
self.$model.sink { value in
guard !(value.name.trimmingCharacters(in: .whitespaces).isEmpty || value.color.trimmingCharacters(in: .whitespaces).isEmpty) else {
self.didUpdate = false
return
}
if value.name != self.model.name {
NSLog("Here")
self.didUpdate = true
}
if value.color != self.model.color {
NSLog("Here")
self.didUpdate = true
}
}
.store(in: &self.cancellables)
}
deinit {
self.cancellables.removeAll()
}
}
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.model.name)
TextField("Lightsaber color", text: self.$viewModel.model.color)
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.navigationBarItems(
trailing:
Button("Save") { NSLog("Saving!") }
.disabled(!self.viewModel.didUpdate)
)
}
}
}
1
.navigationBarItems
已弃用。请改用 .toolbar
。
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") { NSLog("Saving!") }
.disabled(!self.viewModel.didUpdate)
}
}
https://developer.apple.com/documentation/swiftui/view/navigationbaritems(leading:trailing:)
https://developer.apple.com/documentation/swiftui/view/toolbar(content:)-5w0tj
2
如果您有多个模型,请确认 Identifiable
、Equatable
协议。
struct Model: Identifiable, Equatable {
var id: UUID = UUID()
var name: String
var color: String
}
我有一个带有支持 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。第二个问题是,当用户通过文本字段更改视图模型时,它实际上并没有触发发布者。
应该如何解决这些问题?
(我考虑过向 ViewModel 中的每个 属性 添加 didSet{}
但是当有很多属性时这非常难看。我还考虑过向文本字段添加修饰符,但我真的更喜欢将此代码放在 ViewModel 中,因为网络更新也可能会更改 ViewModel)。
有一种更简单的方法可以完成您想要的操作,但是此选项将来可能不是您想要的。但这一切归结为状态的可变性。
首先,您似乎混淆了 Model
和 ViewModel
。在您的情况下,模型应该是这样的:
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
。每当模型与原始模型不同时,保存按钮将被启用,当它相同时,保存被禁用。 重要:只有当您使用 struct
作为模型时,这种编写模型的巧妙方法才有可能,因为它们具有值语义。也正是出于这个原因,在为您的任务建模时 structs
优于 类。
现在,如果您坚持使用您的 ViewModel
(例如,因为无法符合 Equatable
或效率低下),您可以做类似的事情。但是,首先请注意这一行
name.publisher
是 name 上的发布者(类型为 Publishers.Sequence<String, Never>
),而不是 @Published
值(实际上类型为 Published<String>.Publisher
)
前者发布字符串的每个字符,即 this
let name = "Qui-Gon Jinn"
let cancel = name.publisher.print().sink { _ in }
打印
Q
u
i
-
...
您真正想要的是名称的预计值,该名称已经是发布者,即
$name.dropFirst().sink { _ in
NSLog("Here")
self.didUpdate = true
}
请注意,您需要删除第一个值,因为模型会在订阅后立即发布。您还可以将所有这些包装到上述模型中并调用模型的发布者(如果它的属性发生变化,它将发布)。
如果您使用 结构 来保存发件人字段的属性,会更容易。
struct Model {
var name: String
var color: String
}
然后,在self.$model.sink { value in}
中比较新值是否与旧值相同或已更改。
class ViewModel: ObservableObject {
@Published var didUpdate = false
@Published var model: Model
private var cancellables: [AnyCancellable] = []
init() {
self.model = Model(name: "Qui-Gon Jinn", color: "green")
self.$model.sink { value in
guard !(value.name.trimmingCharacters(in: .whitespaces).isEmpty || value.color.trimmingCharacters(in: .whitespaces).isEmpty) else {
self.didUpdate = false
return
}
if value.name != self.model.name {
NSLog("name did chanage")
self.didUpdate = true
}
if value.color != self.model.color {
NSLog("Color did change")
self.didUpdate = true
}
}
.store(in: &self.cancellables)
}
deinit {
self.cancellables.removeAll()
}
}
所有代码
struct Model {
var name: String
var color: String
}
class ViewModel: ObservableObject {
@Published var didUpdate = false
@Published var model: Model
private var cancellables: [AnyCancellable] = []
init() {
self.model = Model(name: "Qui-Gon Jinn", color: "green")
self.$model.sink { value in
guard !(value.name.trimmingCharacters(in: .whitespaces).isEmpty || value.color.trimmingCharacters(in: .whitespaces).isEmpty) else {
self.didUpdate = false
return
}
if value.name != self.model.name {
NSLog("Here")
self.didUpdate = true
}
if value.color != self.model.color {
NSLog("Here")
self.didUpdate = true
}
}
.store(in: &self.cancellables)
}
deinit {
self.cancellables.removeAll()
}
}
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.model.name)
TextField("Lightsaber color", text: self.$viewModel.model.color)
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.navigationBarItems(
trailing:
Button("Save") { NSLog("Saving!") }
.disabled(!self.viewModel.didUpdate)
)
}
}
}
1
.navigationBarItems
已弃用。请改用 .toolbar
。
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") { NSLog("Saving!") }
.disabled(!self.viewModel.didUpdate)
}
}
https://developer.apple.com/documentation/swiftui/view/navigationbaritems(leading:trailing:)
https://developer.apple.com/documentation/swiftui/view/toolbar(content:)-5w0tj
2
如果您有多个模型,请确认 Identifiable
、Equatable
协议。
struct Model: Identifiable, Equatable {
var id: UUID = UUID()
var name: String
var color: String
}