3

I'm very new to RxSwift and RxCocoa and I've recently made heavy use of Variable because of how convenient it is to just push mutations into the Variable through its value. Now that it is deprecated I'm trying to understand how best to use BehaviorRelay instead. There's an Rx-y way of doing what I want to do, but I'm having a hard time landing on it.

What I want is to put an instance of struct-based model behind a ViewModel and observe changes to it and bind UI elements in such a way that I can mutate that model through the BehaviorRelay.

The model is simple:

struct Pizza {
    var name: String
    var price: Float
}

So is the View Model:

final class PizzaViewModel {
    let pizzaRelay =  BehaviorRelay<Pizza>(value: Pizza(name: "Sausage", price: 5.00))

    init(pizza: Pizza) {
        pizzaRelay.accept(pizza)

        // I feel like I'm missing something Rx-like here... 
    }
}

Then somewhere you would maybe bind a UITextField to the BehaviorRelay like so:

viewModel
    .pizzaRelay
    .asObservable()
    .map { $0.name }
    .bind(to: nameTextField.rx.text)
    .disposed(by: disposeBag)

The question becomes: if you need to push values from the text field back into the BehaviorRelay how should that work?

nameTextField
    .rx
    .controlEvent([.editingChanged])
    .asObservable()
    .subscribe(onNext: { [weak self] in
        guard let self = self else { return }
        if let text = self.nameTextField.text {
            self.viewModel.pizzaRelay.value.name = text  // does not compile because value is a let
        }
    }).disposed(by: disposeBag)

I'm probably not using the correct types here or I'm not thinking in the correct Rx-fashion in-terms of streams of inputs/outputs, but I'm curious how others might approach this problem?

Other things I've considered:

  • Just reconstructing a new Pizza in the .subscribe using current value in the BehaviorRelay, mutating the name and then .accept-ing that back into the relay. That doesn't feel exactly right, though.
  • Creating individual BehaviorRelay's for each property I want to mutate on my Pizza, then .accept-ing values for each property and then using combineLatest on all those relays and returning a Observable<Pizza>. But that feels clunky also.

How should this work in an ideal world? Am I thinking about this incorrectly? Help! My head hurts.

4

1 回答 1

1

在理想的世界中,您不会将中继甚至主题用于此类代码。您应该从流程开始,而不是从结构开始。数据应该如何通过您的系统?

例如,这是一个带有视图模型的视图控制器,可以将华氏温度转换为摄氏温度并返回:

struct TempInOut {
    let fahrenheit: Observable<String>
    let celsius: Observable<String>
}

func tempViewModel(input: TempInOut) -> TempInOut {
    let celsius = input.fahrenheit
        .compactMap { Double($0) }
        .map { ($0 - 32) * 5.0/9.0 }
        .map { "\($0)" }

    let fahrenheit = input.celsius
        .compactMap { Double($0) }
        .map { $0 * 5.0/9.0 + 32 }
        .map { "\($0)" }

    return TempInOut(fahrenheit: fahrenheit, celsius: celsius)
}

主要要了解的是数据如何从 input.fahrenheit 流向 output.celsius,以及它如何从 input.celsius 流向 output.fahrenheit。

这是考虑程序的不同方式......我最近听说了“时间设计”的概念,我认为这是一个很好的艺术术语。

这是使用上述视图模型的视图控制器。

class ViewController: UIViewController {

    @IBOutlet weak var fahrenheitField: UITextField!
    @IBOutlet weak var celsiusField: UITextField!

    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        let input = TempInOut(
            fahrenheit: fahrenheitField.rx.text.orEmpty.asObservable(),
            celsius: celsiusField.rx.text.orEmpty.asObservable()
        )

        let output = tempViewModel(input: input)

        disposeBag.insert(
            output.fahrenheit.bind(to: fahrenheitField.rx.text),
            output.celsius.bind(to: celsiusField.rx.text)
        )
    }
}
于 2020-01-24T01:52:12.567 回答