4

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.

Aaron
  • 7,055
  • 2
  • 38
  • 53

1 Answers1

1

In an ideal world, you wouldn't use Relays or even Subjects for such code. Instead of starting with a struct, you should start with a flow. How should data move through your system?

As an example, here is a view controller with view model that can convert Fahrenheit to Celsius and back:

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)
}

The main thing to understand is how the data flows from input.fahrenheit to output.celsius, and how it flows from input.celsius to output.fahrenheit.

It's a different way of thinking about your program... I recently heard about the notion of "temporal design" and I think that's a good term of art for it.

Here is the view controller that would use the above view model.

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)
        )
    }
}
Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • this is super helpful. It feels like having anything _other_ than an Observable is an anti-pattern. Is that a proper way to feel? Some follow up questions: What if your ViewModel needs to take some state, like a complex object? Should we rethink our ViewModel and how state is managed? Would you create Observables for the properties on that complex object and expose those? Thanks again for the info. – Aaron Jan 24 '20 at 17:22
  • Gonna start with some required reading: https://medium.com/@danielt1263/integrating-rxswift-into-your-brain-and-code-base-1a790c36c36d ;-) – Aaron Jan 24 '20 at 17:25
  • 1
    Sometimes you have to use a Subject, but it is something that new people tend to lean on too heavily because it feels more familiar. As for dealing with complex objects, you might enjoy checking out my sample app: https://github.com/danielt1263/RxEarthquake which uses an Earthquake object taken from a server. – Daniel T. Jan 24 '20 at 17:29
  • I'm checking out your github repos right now. This is fantastic content. Thank you for putting it out there. – Aaron Jan 24 '20 at 17:30
  • One of the things that is challenging is understanding what each Type in the RxSwift framework is meant to be used for. I think `Observable` is well named, but `BehaviorRelay` or `BehaviorSubject` are difficult to pin down and define relative to what you know about how your application works. Do you know of any resources that attempt to define these things in simple terms that an average iOS dev would understand? – Aaron Jan 24 '20 at 22:13