【问题标题】:RxSwift, ViewModels and UI-bindingsRxSwift、ViewModels 和 UI 绑定
【发布时间】:2018-10-04 13:41:57
【问题描述】:

我想知道在以下情况下最佳做法是什么:

  • 项目设置为模型-视图-视图模型
  • 我需要显示一个带有文本字段的屏幕(例如name),用户可以在其中编辑特定Model 的详细信息
  • ViewModel 包含一个Observable<Model>; ViewModel 负责从 API 中检索数据
  • 视图包含需要使用来自Model 的当前数据预填充的输入字段
  • 输入字段需要绑定到例如ViewModel 中的BehaviorRelays

我正在努力寻找解决此问题的最佳方法:

  • 我不想在 ViewModel 中订阅
  • 我希望 ViewModel 负责检索数据并填充 BehaviorRelays 或类似名称

我基本上想要一个类似于BehaviorRelays 的东西,但初始值来自Observable。在订阅(绑定) BehaviorRelays 后,我希望他们从 API 中检索数据。还是 flatMapObservableBehaviorRelay

解决这个问题的最佳方法是什么?

我以前实现过的方法及其缺点:

  1. 跳过 ViewModel 中的BehaviorRelays,并将状态保留在 ViewController 中。只需从 ViewController 订阅 ViewModel 中的 Observable<Model> 并将其绑定到 UI。然后,当用户想要保存数据时,从 UI (withLatestFrom()) 检索最新值并将其作为值传递给 ViewModel 中的func save(attribute: Value)。这可行,但我想将所有状态(和业务逻辑)保留在 ViewModel 中。
  2. 在ViewModel中,使用BehaviorRelay,初始值为空,然后在ViewModel的init()中,检索数据并绑定到BehaviorRelays。这意味着在 ViewModel 中有订阅,我想避免这种情况。
  3. 在 ViewModel 中有类似 func refresh() -> Completable 的东西,它将 BehaviorRelays 中的值设置在其 do(onNext:) 中。这行得通,但感觉很老套。
  4. 在 ViewModel 中有一个let refresh = PublishSubject<()>(),例如

    var model: Observable<Model> {
      refresh
        .startWith(())
        .flatMapLatest { retrieveData() }
    }
    

    再次使用do(onNext:)。与以前的解决方案相同的缺点(感觉很老套)。

  5. 在初始化 ViewModel 之前检索数据,并使用模型实例初始化 ViewModel。我不喜欢这种解决方案,因为我希望 ViewModel 负责检索它自己的数据。

还有其他方法吗?

【问题讨论】:

    标签: swift mvvm rx-swift


    【解决方案1】:

    我的第一点是,我认为您需要更多地考虑 ViewModel 在 MVVM 中的作用。视图模型的目的是转换/修改模型中的数据,使其非常适合在该屏幕上显示。因此,如果您正在制作一个充满可编辑字段的屏幕,那么 ViewModel 应该将数据拆分为这些离散字段。在 Rx 中,这通常意味着为每个字段公开一个 Observable(或 Driver,如果需要)。目标是使视图模型中每个属性的绑定简单。视图不应执行任何属于 VM 的表示转换/逻辑。

    您的 VM 不应该进行任何订阅是正确的,ViewController(在 MVVM 中查看)将对它显示的每个字段进行订阅。您可以让 viewmodel 在调用 API 时使用 .share(),并使用生成的 observable 来提供每个字段 observables,这样当视图订阅它们时,它们就不会都各自调用 API。

    我不确定您需要 BehaviorRelay 指的是什么。也许您可以澄清那里的需求。如果是因为您想要一种触发刷新的方法,那么您的第 4 颗子弹就很有意义。你可以这样设置

    让 fetchDataTrigger = PublishRelay();

    let dataModelObservable = fetchDataTrigger.asObservable()
        .startWith(())
        .flatMap {
            return goCallMyApi()
        }
        .share()
    

    然后您的每个属性都可以转换dataModelObservable 的输出以生成该字段的值。如果您想触发刷新,只需调用 fetchDataTrigger.accept(()) 以响应您的按钮按下或任何触发刷新的操作。

    【讨论】:

    • 感谢您的帮助!我确实转换了 ViewModel 中的数据以便在 View 中直接使用,但为了简洁起见,示例保持简单。为什么要使用 BehaviorRelays 是因为我想在 ViewModel 中的 BehaviorRelay 和 ViewController 中的 UITextField 之间进行双向绑定。
    • 我还是不明白你为什么需要一个 BehaviorRelay。当用户更改文本字段时应该采取什么行为?它是否将某些内容发送回服务器?如果是这样,当您通过调用 VM 中的方法来更改视图模型(和服务器)中该字段的值时,您可以调用 fetchDataTrigger.accept(()) 来执行此操作。 Rx 的另一个重要方面是使用单向不可变数据。始终假设从 API 向 UI 移动的数据是不可变的和单向的。如果您需要修改某些内容,那么它应该会导致该流程中的另一个事件来更新 UI
    • 也许我只是误解了一些东西,但假设我们有一个用于编辑用户配置文件的屏幕。然后我会有一个包含let name = BehaviorRelay&lt;String&gt;(value: "") 的 EditProfileViewModel。我在 UI 中绑定了这个,例如:let nameTextField = UITextField()nameTextField.rx.text.bind(to: viewModel.name)。然后在 ViewModel 中,我有一个 func save() -&gt; Completable,它从 BehaviorRelays 获取当前值并将它们发布到 API。这适用于新用户配置文件,但对于现有用户,UITextField 需要预先填充当前值。
    • 使用flatMapLatest 代替flatMap 进行网络调用。
    • 好的,如果你想保持一个“状态”,你可以使用 UI 中每个字段的 observable(例如 textField.rx.text)并添加 distinctUntilChanged().replay(1) 并将它们设置到你的 viewModel 中。然后,当您想将数据发送到服务器时,您可以将所有这些压缩起来,将其转换为 API 并发送。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-12-29
    • 2017-09-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多