使用SwiftUI和Combine进行双向绑定

时间:2019-11-27 16:51:46

标签: swiftui combine

我正在尝试找出如何在父子关系中的两个ViewModel之间正确传递对象或一组值的方法,以便在更新子ViewModel时,更改会返回到父级。

当仅使用SwiftUI视图并将其直接绑定到商店时,这非常简单,但我想保留用于字段验证的业务逻辑,等等,与SwiftUI视图分开。

下面的代码显示了父级更新时子级的更新(按预期),但是我需要以某种方式将子级中已更改的值传递回父级。我对移动应用程序开发非常陌生,仍然在学习,所以我确定我缺少一些非常简单的东西。

import SwiftUI
import Combine

struct Person: Hashable {
  var givenName: String
  var familyName: String
}

// my person store - in the real app it's backed by coredata
class PersonStore: ObservableObject {
  @Published var people: [Person] = [
    Person(
      givenName: "Test",
      familyName: "Person"
    )
  ]
  static let shared = PersonStore()
}

// app entrypoint
struct PersonView: View {
  @ObservedObject var viewModel: PersonView_ViewModel = PersonView_ViewModel()

  var body: some View {
    NavigationView {
      VStack {
        List(viewModel.people.indices, id: \.self) { idx in
          NavigationLink(destination: PersonDetailView(viewModel: PersonDetailView_ViewModel(personIndex: idx))) {
            Text(self.viewModel.people[idx].givenName)
          }
        }
      }
    }
  }
}

class PersonView_ViewModel: ObservableObject {
  @Published var people: [Person] = PersonStore.shared.people
}

// this is the detail view
struct PersonDetailView: View {
  @ObservedObject var viewModel: PersonDetailView_ViewModel

  var body: some View {
    Form {
      Section(header: Text("Parent View")) {
        VStack {
          TextField("Given Name", text: self.$viewModel.person.givenName)
          Divider()
          TextField("Family Name", text: self.$viewModel.person.familyName)
        }
      }
      PersonBasicDetails(viewModel: PersonBasicDetails_ViewModel(person: viewModel.person))
    }
  }
}

// viewmodel associated with detail view
class PersonDetailView_ViewModel: ObservableObject {
  @Published var person: Person

  init(personIndex: Int) {
    self.person = PersonStore.shared.people[personIndex]
  }
}

// this is the child view - in the real app there are multiple sections which are conditionally rendered
struct PersonBasicDetails: View {
  @ObservedObject var viewModel: PersonBasicDetails_ViewModel

  var body: some View {
    Section(header: Text("Child View")) {
      VStack {
        TextField("Given Name", text: self.$viewModel.person.givenName)
        Divider()
        TextField("Family Name", text: self.$viewModel.person.familyName)
      }
    }
  }
}

class PersonBasicDetails_ViewModel: ObservableObject {
  @Published var person: Person

  init(person: Person) {
    self.person = person
  }
}

struct PersonView_Previews: PreviewProvider {
  static var previews: some View {
    PersonView()
  }
}

2 个答案:

答案 0 :(得分:1)

在网络上的大多数SwiftUI TextField示例中,绑定都是通过使用@State变量来提供的,该变量为您创建了Binding的实例。

但是,您也可以使用Binding构造函数创建自定义绑定。这是一个看起来像的例子:

TextField(
  "Given Name",
  text: Binding(
    get: { self.$viewModel.person.givenName },
    set: { self.$viewModel.person.givenName = $0 }))

答案 1 :(得分:0)

如果要双向运行,不仅需要发布,还必须使用向上绑定。

                    struct Person: Hashable {
                      var givenName: String
                      var familyName: String
                    }

                    // my person store - in the real app it's backed by coredata
                    class PersonStore: ObservableObject {
                      @Published var people: [Person] = [
                        Person(
                          givenName: "Test",
                          familyName: "Person"
                        )
                      ]
                      static let shared = PersonStore()
                    }

                    // app entrypoint
                    struct PersonView: View {
                      @ObservedObject var viewModel: PersonView_ViewModel = PersonView_ViewModel()

                      var body: some View {
                        NavigationView {
                          VStack {
                            List(viewModel.people.indices, id: \.self) { idx in
                                NavigationLink(destination: PersonDetailView(viewModel: PersonDetailView_ViewModel(person: self.$viewModel.people , index: idx ))) {
                                Text(self.viewModel.people[idx].givenName)
                              }
                            }
                          }
                        }
                      }
                    }

                    class PersonView_ViewModel: ObservableObject {
                      @Published var people: [Person] = PersonStore.shared.people
                    }

                    // this is the detail view
                    struct PersonDetailView: View {
                      @ObservedObject var viewModel: PersonDetailView_ViewModel

                      var body: some View {
                        Form {
                          Section(header: Text("Parent View")) {
                            VStack {
                              TextField("Given Name", text: self.viewModel.person.givenName)
                              Divider()
                              TextField("Family Name", text: self.viewModel.person.familyName)
                            }
                          }
                          PersonBasicDetails(viewModel: PersonBasicDetails_ViewModel(person: viewModel.person))
                        }
                      }
                    }

                    // viewmodel associated with detail view
                    class PersonDetailView_ViewModel: ObservableObject {
                       @Published var person: Binding<Person>

                        init(person: Binding<[Person]> ,index: Int) {
                        self.person = person[index]
                      }
                    }

                    // this is the child view - in the real app there are multiple sections which are conditionally rendered
                    struct PersonBasicDetails: View {
                      @ObservedObject var viewModel: PersonBasicDetails_ViewModel

                      var body: some View {
                        Section(header: Text("Child View")) {
                          VStack {
                            TextField("Given Name", text: self.viewModel.person.givenName)
                            Divider()
                            TextField("Family Name", text: self.viewModel.person.familyName)
                          }
                        }
                      }
                    }

                    class PersonBasicDetails_ViewModel: ObservableObject {
                       @Published var person: Binding<Person>

                     init(person: Binding<Person>) {
                        self.person = person //person
                     }

                    }

                    struct PersonView_Previews: PreviewProvider {
                      static var previews: some View {
                        PersonView()
                      }
                    }