我试图传递从一个视图模型到另一个发布的值(即子视图模型需要访问源并能够操纵值)。我敢打赌这很简单,然而,我似乎找不到"正确的"。在MVVM模式下的工作方式。
我试过使用@Bindings
,Binding<Value>
。我设法让它与@EnvironmentObject
工作,但这意味着视图必须处理它,因为我不能把它传递到视图模型逻辑应该在哪里(最终我想使用组合来操纵子视图模型中的数据流)。我错过了什么?
我用下面的游乐场代码简化了这种情况:
import SwiftUI
import PlaygroundSupport
class InitialViewModel: ObservableObject {
@Published var selectedPerson: Person = Person(firstName: "", surname: "")
}
struct InitialView: View {
@StateObject var viewModel = InitialViewModel()
var body: some View {
ButtonView(selectedPerson: Published(wrappedValue: viewModel.selectedPerson) )
SelectedPersonView(selectedPerson: Published(wrappedValue: viewModel.selectedPerson))
}
}
class ButtonViewModel: ObservableObject {
@Published var selectedPerson: Person
init(selectedPerson: Published<Person>) {
self._selectedPerson = selectedPerson
}
func toggleSelectedPerson() {
if selectedPerson.firstName.isEmpty {
selectedPerson = Person(firstName: "Boris", surname: "Johnson")
} else {
selectedPerson = Person(firstName: "", surname: "")
}
}
}
struct ButtonView: View {
@ObservedObject var viewModel: ButtonViewModel
init(selectedPerson: Published<Person>) {
self._viewModel = ObservedObject(wrappedValue: ButtonViewModel(selectedPerson: selectedPerson))
}
var body: some View {
Button(action: { viewModel.toggleSelectedPerson()} ) {
Text("Press to select person")
}
}
}
class SelectedPersonViewModel: ObservableObject {
@Published var selectedPerson: Person
init(selectedPerson: Published<Person>) {
self._selectedPerson = selectedPerson
}
}
struct SelectedPersonView: View {
@ObservedObject var viewModel: SelectedPersonViewModel
init(selectedPerson: Published<Person>) {
self._viewModel = ObservedObject(wrappedValue: SelectedPersonViewModel(selectedPerson: selectedPerson))
}
var body: some View {
if viewModel.selectedPerson.firstName.isEmpty {
Text("No person selected yet")
} else {
Text("Person (viewModel.selectedPerson.firstName) selected!")
}
}
}
struct Person {
let firstName: String
let surname: String
}
let view = InitialView()
PlaygroundPage.current.setLiveView(view)
实际上,当我按下按钮时,selectedPerson
属性应该更新,视图应该相应地更新。
8月19日编辑
好吧,为了澄清这个问题,我添加了一个非常简化的版本的实际代码,我正在工作。希望这能解释为什么我在看这个问题。
注意:我知道编译错误。这只是为了演示我正在寻找的内容。
struct ItemOption: Identifiable {
let id: Int
let name: String
var dependentOn: [Int]? // dependencies where, choosing one option opens up more options
let values: [ItemOptionValue]
}
struct ItemOptionValue: Identifiable {
let id: Int
let name: String?
}
class OptionViewModel: ObservableObject { // all options e.g. config options on a car
@Published var selectedOptions = [Int:[Int]]() // Structure of [OptionID: [ValueID]
@Published var allOptions = [ItemOption]()
@Published var filteredOptions = [ItemOption]()
init(options: [ItemOption]) {
self.allOptions = options
filterAvailableOptions()
}
func filterAvailableOptions() {
// Combine code to filter viewable options depending on dependencies that may appear in selectedOption
}
}
struct OptionView: View {
@StateObject var viewModel: OptionViewModel
init(options: [ItemOption]) {
self.viewModel = OptionViewModel(options: options)
}
var body: some View {
ForEach(viewModel.filteredOptions) { section in
OptionTypeView(selectedOptions: viewModel.selectedOptions, optionValues: section.values)
}
}
}
class OptionTypeViewModel: ObservableObject { // each option type e.g. colour on car, wheel trims etc
}
struct OptionTypeView: View {
var selectedOptions: [Int:[Int]]
var optionValues: [ItemOptionValue]
@StateObject var viewModel = OptionTypeViewModel()
var body: some View {
ForEach(optionValues) { value in
OptionValueView(selectedOptions: selectedOptions)
}
}
}
class OptionValueViewModel: ObservableObject { // values of each option e.g. each colour choice
@Published var isOptionSelected: Bool
@Published var selectedOptions: [Int:[Int]] // passed on value (Binding?) from OptionViewModel
init(selectedOptions: [Int:[Int]]) {
self.selectedOptions = selectedOptions
}
func trackSelectedOptions(optionID: Int, valueID: Int) {
$selectedOptions
// ... Combine mapping to check if value exists in selectedOptions that originally comes from OptionViewModel
.assign(.isOptionSelected, on: self)
}
func removeOption() {
// code to remove value id from selectedOptions
}
func addOption() {
// code to add value id to selectedOptions
}
}
struct OptionValueView: View {
@StateObject var viewModel: OptionValueViewModel
init(selectedOptions: [Int: [Int]]) {
self.viewModel = OptionValueViewModel(selectedOptions: selectedOptions)
}
var body: some View {
if viewModel.isOptionSelected {
Text("Option is selected")
Button(action: { viewModel.removeOption } ) {
Text("Remove option")
}
} else {
Text("Option is NOT selected")
Button(action: { viewModel.addOption } ) {
Text("Add option")
}
}
}
}
一个选项可能是在一个大的视图(与视图模型)和函数/计算变量处理子视图为了有访问原始值,但问题仍然存在,OptionValueView仍然需要计算它自己的值,以驱动什么显示在视图上,这需要在自己的视图模型中完成。
遇到https://mokacoding.com/blog/swiftui-dependency-injection/后,我想我已经找到了一个解决方案,可以很好地工作,使用创建视图模型的ViewModelFactory
并直接注入仅创建一次的PersonController
。我甚至可以潜在地在InitialView
中创建ViewModelFactory
作为EnvironmentObject
,并在需要进一步创建视图模型的情况下将其传递给孙子视图。
import SwiftUI
import Combine
import PlaygroundSupport
struct Person {
let firstName: String
let surname: String
}
class PersonController: ObservableObject {
@Published var selectedPerson: Person = Person(firstName: "", surname: "")
}
class ViewModelFactory {
let personController = PersonController()
func makeButtonViewModel() -> ButtonViewModel {
return ButtonViewModel(personController: personController)
}
func makeSelectedPersonViewModel() -> SelectedPersonViewModel {
return SelectedPersonViewModel(personController: personController)
}
}
class InitialViewModel: ObservableObject {
let viewModelFactory = ViewModelFactory()
}
struct InitialView: View {
@StateObject var viewModel = InitialViewModel()
var body: some View {
ButtonView(viewModel: viewModel.viewModelFactory.makeButtonViewModel())
SelectedPersonView(viewModel: viewModel.viewModelFactory.makeSelectedPersonViewModel())
}
}
class ButtonViewModel: ObservableObject {
private let personController: PersonController
init(personController: PersonController) {
self.personController = personController
}
func toggleSelectedPerson() {
if personController.selectedPerson.firstName.isEmpty {
personController.selectedPerson = Person(firstName: "Boris", surname: "Johnson")
} else {
personController.selectedPerson = Person(firstName: "", surname: "")
}
}
}
struct ButtonView: View {
@ObservedObject var viewModel: ButtonViewModel
var body: some View {
Button(action: { viewModel.toggleSelectedPerson()} ) {
Text("Press to select person")
}
}
}
class SelectedPersonViewModel: ObservableObject {
@Published var selectedPerson: Person
private let personController: PersonController
private var cancellables = Set<AnyCancellable>()
init(personController: PersonController) {
self.personController = personController
selectedPerson = personController.selectedPerson
personController.$selectedPerson
.sink { [weak self] in
self?.selectedPerson = $0
}
.store(in: &cancellables)
}
}
struct SelectedPersonView: View {
@ObservedObject var viewModel: SelectedPersonViewModel
var body: some View {
if viewModel.selectedPerson.firstName.isEmpty {
Text("No person selected yet")
} else {
Text("Person (viewModel.selectedPerson.firstName) selected!")
}
}
}
let view = InitialView()
PlaygroundPage.current.setLiveView(view)
我认为你应该这样做
ButtonView(selectedPerson: viewModel._selectedPerson) )
SelectedPersonView(selectedPerson: viewModel._selectedPerson)
我怀疑以上是否真的会将值从子节点传播到父节点@Published
但:
你可以把闭包传递给子视图模型,这个闭包通过它的参数将这个事件传播给父视图模型,并设置@Published
final class ChildViewModel {
init(onSelectedPerson: (Person) -> Void) { }
}
或
PassthroughSubject
传递给子视图。