Reactivecocoa: Provide SignalProducers to observe component's values

Created on 23 Nov 2016  Â·  8Comments  Â·  Source: ReactiveCocoa/ReactiveCocoa

I'm a big fan of the Reactive approach, great job with this @andersio, but I think we lost some flexibility along the way.

Let's take for example a UITextField.

// Rex (RAC4)
var rex_text: MutableProperty<String?>

// RAC5
var textValues: Signal<String?, NoError>
var continuousTextValues: Signal<String?, NoError>

Using Rex, and because it was using a MutablePropery to provide the binding, we have a flexibility to choose between using a signal if we are interested on following changes or a producer if we want the current value followed by any change over time. Depending on how you are using this bindings it can be quite useful to have both options.

An example can be something like a login button being enabled if we have a valid username and password or disabled otherwise.

loginButton.rex_enabled <~ 
    combineLatest(usernameTextField.rex_text.producer, passwordTextField.rex_text.producer) // I'm using `.producer` just to empathise this
        .map { $0?.characters.count ?? 0 > 0 && $1?.characters.count ?? 0 > 0 }

We can argue that probably we should be using an Action but I don't think everyone is using or is going to use them. This issue was recently reported here: https://github.com/ReactiveCocoa/ReactiveSwift/issues/99.

Another example could be using UIDatePicker's date even when the user doesn't change the pre-selected date.

let dateProperty: MutableProperty<NSDate?> = NSMutableProperty(nil)
...
// Our property will contain always the date of the picker
dateProperty <~ datePicker.rex_date.producer

Using RAC5 we have signals to observe any changes which covers a lot of situations but we loose the ability to use the current value and following changes, if there are any, in an easy way.

What I would like to propose is introduce a parallel SignalProducer that triggers both the current value of the control followed by any changes over time giving the ability for callers to choose which behaviour they really want without needing to find a workaround to restore the previous functionality.

proposal

Most helpful comment

It's a good points but I don't think we should assume, and possibly become biased[…]

That's kind of the point of API Design (and design in general.) You want to guide the user of your product towards the good behavior, if possible.

Granted, what you're saying is incorrect by suggesting that we're against MVVM in any way. In fact, this behavior change is an example of fitting even better with MVVM.

That is, if the ViewModel holds the "truth" of what is displayed by the View(Controller), then it's going to push values out to the view, and accept changes from it. If anything, your example above has a bug if it is adhering to MVVM:

loginButton.rex_enabled <~ 
    combineLatest(usernameTextField.rex_text.producer, passwordTextField.rex_text.producer) // I'm using `.producer` just to empathise this
        .map { $0?.characters.count ?? 0 > 0 && $1?.characters.count ?? 0 > 0 }

Instead, what you wrote is better off defined by the ViewModel:

class ViewModel {
    var username: MutableProperty<String>
    var password: MutableProperty<String>
    var loginEnabled: Property<Bool>

    // This can be done in a different spot, or init can be made to take initial values as parameters, or whatever...
    init() {
        userName = MutableProperty("")
        password = MutableProperty("")

        // I'm pretty sure the below code is incorrect, but I'm sure you get the idea that we're creating a "calculated, read-only property" out of the above two.
        loginEnabled = .combineLatest(userName.producer, password.producer).map { $0.0.characters.count > 0 && $0.1.characters.count > 0 }
    }
}

And now, in your ViewController:

loginButton.reactive.enabled <~ viewModel.loginEnabled

// Only take the initial values here, in case they are persisted elsewhere. 
// N.B.: If you expect the underlying model values to change underneath you, this has to be very different code…
usernameTextField.reactive.text <~ viewModel.userName.take(first: 1)
passwordTextField.reactive.text <~ viewModel.password.take(first: 1)

viewModel.userName <~ usernameTextField.reactive.continuousText
viewModel.password <~ passwordTextField.reactive.continuousText

Anyway, it's very common for the MVVM stuff to "leak all over" in many examples I've found online. People end up splitting the ViewModel class' responsibilities between the Model and the View, and the ViewModel doesn't end up doing much…

So, yeah—this needed to change because it was obviously being abused. The fact that the example you shared no longer works is By Design. 😄

All 8 comments

The argument that brought up by @liscio that were agreed on is that controls should not be the source of truth.

Based on this principle, I am not sure how such producers would fit. One is supposed to have initialised the controls in the desired state, and have established observations to them at the very beginning of the UIControl's lifetime.

https://github.com/ReactiveCocoa/ReactiveCocoa/issues/3229

I would agree with @andersio and @liscio. I see the behaviour/semantics of a UI Control as being hot. So I would say:

  1. > established observations to them at the very beginning
  2. Concat the initial UI state with the Signal's values and observe that.

Another observation is that you seems to have presentation logic in your example that probably belongs to the controller or the view model.

.map { $0?.characters.count ?? 0 > 0 && $1?.characters.count ?? 0 > 0 }

This is exactly the kind of logic that is expected to be executed by the view model, since it is supposed to know all the states related to the view it is associated with.

So assuming if you do the "VC first" approach, the VC would essentially just establish bindings like these.

loginButton.isEnabled <~ viewModel.canSubmit
viewModel.username <~ usernameTextField.reactive.continuousTextValues
viewModel.password <~ passwordTextField.reactive.continuousTextValues

With the logic being in the view model:

isEnabled <~ viewModel.username.combineLatest(with: viewModel.password)
    .map { $0?.characters.count ?? 0 > 0 && $1?.characters.count ?? 0 > 0 }

where the initial values are determined by the view model.

It's a good points but I don't think we should assume, and possibly become biased, about a specific design pattern like MVVM despite the fact ReactiveCocoa is popularly adopted along with MVVM.

I think abandoning a MutableProperty was an excellent option because I believe most of the times it was implicitly being used as a producer over a signal potentially leading to situations like the "Hello World" that @liscio has referred, but with this explicit separation it's a decision of the caller to choose between which semantic is really expected.

But @RuiAAPeres I agree with you, it's undeniable that a UI Control should be seen as being hot and this enforces that, my only concern is that we are compromising some flexibility that we currently have. It's not a deal breaker but won't facilitate the migration between RAC4 to RAC5.

The example I gave works with Cocoa MVC too. It is just to illustrate the idea how line should probably be drawn between the view and the mediating layer, regardless of any design patterns. If it does not own the state, having something that emits the "current value" seems making little sense.

Say you can set all these states in VC alongside establishing these bindings, which I feel a bit better solution than perhaps depending on the default initialised state.

I am hesitate to clutter the API for just a transitive purpose.

It's a good points but I don't think we should assume, and possibly become biased[…]

That's kind of the point of API Design (and design in general.) You want to guide the user of your product towards the good behavior, if possible.

Granted, what you're saying is incorrect by suggesting that we're against MVVM in any way. In fact, this behavior change is an example of fitting even better with MVVM.

That is, if the ViewModel holds the "truth" of what is displayed by the View(Controller), then it's going to push values out to the view, and accept changes from it. If anything, your example above has a bug if it is adhering to MVVM:

loginButton.rex_enabled <~ 
    combineLatest(usernameTextField.rex_text.producer, passwordTextField.rex_text.producer) // I'm using `.producer` just to empathise this
        .map { $0?.characters.count ?? 0 > 0 && $1?.characters.count ?? 0 > 0 }

Instead, what you wrote is better off defined by the ViewModel:

class ViewModel {
    var username: MutableProperty<String>
    var password: MutableProperty<String>
    var loginEnabled: Property<Bool>

    // This can be done in a different spot, or init can be made to take initial values as parameters, or whatever...
    init() {
        userName = MutableProperty("")
        password = MutableProperty("")

        // I'm pretty sure the below code is incorrect, but I'm sure you get the idea that we're creating a "calculated, read-only property" out of the above two.
        loginEnabled = .combineLatest(userName.producer, password.producer).map { $0.0.characters.count > 0 && $0.1.characters.count > 0 }
    }
}

And now, in your ViewController:

loginButton.reactive.enabled <~ viewModel.loginEnabled

// Only take the initial values here, in case they are persisted elsewhere. 
// N.B.: If you expect the underlying model values to change underneath you, this has to be very different code…
usernameTextField.reactive.text <~ viewModel.userName.take(first: 1)
passwordTextField.reactive.text <~ viewModel.password.take(first: 1)

viewModel.userName <~ usernameTextField.reactive.continuousText
viewModel.password <~ passwordTextField.reactive.continuousText

Anyway, it's very common for the MVVM stuff to "leak all over" in many examples I've found online. People end up splitting the ViewModel class' responsibilities between the Model and the View, and the ViewModel doesn't end up doing much…

So, yeah—this needed to change because it was obviously being abused. The fact that the example you shared no longer works is By Design. 😄

I've recently answered a similar question on stackoverflow where the user was originally observing the enabled property of a UIButton.

I'm not sure if I even should have provided the first part of the answer since I agree with @liscio

Was this page helpful?
0 / 5 - 0 ratings