【问题标题】:How to pass published values between view models?如何在视图模型之间传递已发布的值?
【发布时间】:2021-08-18 16:01:00
【问题描述】:

我正在尝试将发布的值从一个视图模型传递到另一个视图模型(即子视图模型需要访问源并能够操作值)。我敢打赌这很简单,但是,我似乎无法在 MVVM 模式中找到“正确”的做法。

我尝试过使用@BindingsBinding<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")
            }
        }
    }
}

一种选择可能是将它们全部连接到一个大型视图(带有视图模型)和处理子视图的 func/computed 变量中,以便访问原始值,但问题仍然是 OptionValueView 仍然需要计算它自己的值以驱动视图上显示的内容,这需要在它自己的视图模型中完成。

【问题讨论】:

  • 这是否与您的实际项目一样复杂?
  • 完全没有,但基本概念保持不变。在实际项目中,子视图需要访问父视图值(在本例中为字典),以便订阅并将日期流操作为子视图所需的子视图模型中的简化动态值 (Int)。
  • 我问是因为你永远不会做三个不同的视图模型只是共享一个人。这一切都将在您与environmentObject 共享的视图模型中完成。如果子视图只需要访问某个值,则只需将该值传递为Binding(如果需要)或将整个视图模型传递为environmentObject,不要创建新的。
  • 我明白你在做什么。它不会像您期望的那样工作,因为您不会像您期望的那样将一个与另一个链接起来。您需要一个集中的“存储”位置来存放您的人员以及多个视图模型可以访问的替代方案。
  • 当然,这取决于您的最终真相来源在哪里。数据库?一份文件?一个网站?视图模型将访问您的管理器/存储以获取数据。它不必是单例。最终,真相应该只有一个来源,那就是您的存储空间。

标签: swift mvvm swiftui


【解决方案1】:

在遇到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)

【讨论】:

  • 我处于类似的位置,但无法找出问题所在。愿意帮我一把吗? stackoverflow.com/questions/71095653/…
  • @ChristopherFrancisco 这个周末我最早可以看看。对不起。
  • 感谢感谢!
猜你喜欢
  • 2011-09-17
  • 2021-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-02
相关资源
最近更新 更多