Reactivecocoa: Why do UI controls expose `Signal`s that do not replay the initial value?

Created on 20 Dec 2016  ·  1Comment  ·  Source: ReactiveCocoa/ReactiveCocoa

Here are pieces explaining why UI controls expose Signals that do not replay the initial value.

Note that it can still be done manually via SignalProducer.prefix(value:) or Property(initial:signal:), but we intentionally avoid it in the public API of ReactiveCocoa as a sign of discouragement.


We discourage anti-patterns and fragile layering.

You only have the isOnValues signal for this purpose but you will not have the very first value, so you will have to make either a producer out of it or a property.

The view itself should not be the source of truth, but a certain property of the controller or the view model that binds with it.

This is especially important if changes are coming in both directions (e.g. user interactions and model layer). UIKit is not KVO compliant. Moreover, all the UI control signals react ONLY to user interactions — they would not send out the changes you programmatically applied. In other words, UI components are not a reliable reactive owner of your states.

Moreover, these uses of SignalProducer or Property in the view layer are usually business logic. Let's say composing fields with a certain constraints to determine the availability of the "Submit" button. These do not belong to the view layer in any mainstream architectural patterns, especially MVVM that is commonly used with RAC/RAS.

The other thing is the isEnableValues signal. Why this signal isn't part of the extension yet?

Whether a control is enabled or not is unanimously decided by whoever owns the view, i.e. the controller or the view model. User interactions would not affect isEnabled in any sense, so there is no point to have it (so as many other properties).

If you ever feel it is needed, it is a sign of a UI component being used to hold application states.

_— https://github.com/ReactiveCocoa/ReactiveCocoa/pull/3347#issuecomment-267738996_.


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. 😄

_— @liscio, https://github.com/ReactiveCocoa/ReactiveCocoa/issues/3310#issuecomment-263395633._


We consider this a sign of fragile layering.

The "truth" should have been maintained in the Controller layer (MVC) or the View Model layer (MVVM), and the business logic should have been computed in those layers instead of in the views. In other words, these _[user interaction driven]_ signals should only be used to update those layers.

Let's look at a classic example: a login page. You may observe three things in the following code snippet:

  1. The source of truth is LoginViewModel.username and LoginViewModel.password.

  2. The view is initialised with the value held by the view model, before the bindings are set.

  3. The business logic which determines the availability of the submit button lives inside the view model. In this snippet, Action implicitly establishes the availability binding for you. But it can also be a Property from which can be bound to a view directly.

    submitButton.isEnabled <~ viewModel.isEnabled
    

    ```swift
    final class LoginViewController: UIViewController {
    @IBOutlet var usernameField: UITextField!
    @IBOutlet var passwordField: UITextField!
    @IBOutlet var submitButton: UIButton!

    var viewModel: LoginViewModel!

    func viewDidLoad() {
    super.viewDidLoad()

    // Initialize the views before setting up the binding.
    
    viewModel.username.withValue { username in
        usernameField.text = username
        viewModel.username <~ usernameField.reactive.continuousTextValues.skipNil()
    }
    
    viewModel.password.withValue { password in
        passwordField.text = password
        viewModel.password <~ passwordField.reactive.continuousTextValues.skipNil()
    }
    
    // Setup the action. The button would be automatically disabled, since
    // the view model "owns" the state and knows a certain initial value that
    // can be used to compute the availability of the submit button.
    submitButton.reactive.pressed = CocoaAction(viewModel.submit)
    

    }
    }

```swift
final class LoginViewModel {
    public let username = MutableProperty<String>("")
    public let password = MutableProperty<String>("")
    public let submit: Action<(), (), NoError>

    init(_ loginManager: LoginManager) {
    let submitEnabled: (String, String) -> Bool = { u, p in
        return u.characters.count >= 3 && p.characters.count > 1
    }

        submit = Action(state: username.combineLatest(with: password),
                        enabledIf: submitEnabled) { state, _ in
        // state: (String, String)
        return loginManager.login(state)
    }
    }
}

This is what we consider proper layering, because LoginViewModel owns all your states. So regardless of how the view is changed, your business logic would be isolated from all the interaction stuff. The view model would have a testable interface too.

The same would also apply to MVC patterns, when one starts to separate different concerns into separate entities.

But it is undoubtedly that we all have different constraints and priorities. So if tweaking your codebase for proper layering is not an realistic goal, SignalProducer.prefix as suggested by @sharplet is the interim solution.

_— https://github.com/ReactiveCocoa/ReactiveCocoa/issues/3352#issuecomment-268423181._

META

Most helpful comment

This should make it into the documentation. ☺️

>All comments

This should make it into the documentation. ☺️

Was this page helpful?
0 / 5 - 0 ratings

Related issues

v-silin picture v-silin  ·  4Comments

Lewion picture Lewion  ·  4Comments

baei2048 picture baei2048  ·  3Comments

BrettThePark picture BrettThePark  ·  4Comments

akashivskyy picture akashivskyy  ·  5Comments