【问题标题】:SwiftUI Combine observing updatesSwiftUI 结合观察更新
【发布时间】:2021-09-29 16:15:19
【问题描述】:

我有一个带有支持 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)。

【问题讨论】:

    标签: swiftui combine


    【解决方案1】:

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

    首先,您似乎将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。每当模型与原始模型不同时,将启用保存按钮,如果相同,则禁用保存。 重要提示:只有当您使用 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
    }
    

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

    【讨论】:

      【解决方案2】:

      如果您使用 struct 来保存 From 字段的属性会更容易。

      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)
          }
      }
      
      
      1. https://developer.apple.com/documentation/swiftui/view/navigationbaritems(leading:trailing:)

      2. https://developer.apple.com/documentation/swiftui/view/toolbar(content:)-5w0tj

      2

      如果您有多个模型,请确认IdentifiableEquatable 协议。

      
      struct Model: Identifiable, Equatable {
          var id: UUID = UUID()
          
          var name: String
          var color: String
      }
      
      

      【讨论】:

        猜你喜欢
        • 2019-11-06
        • 1970-01-01
        • 2021-05-27
        • 1970-01-01
        • 1970-01-01
        • 2020-07-29
        • 2021-06-27
        • 2021-05-14
        • 1970-01-01
        相关资源
        最近更新 更多