Reactivecocoa: Control bindings for `Action`s.

Created on 4 Oct 2016  ·  22Comments  ·  Source: ReactiveCocoa/ReactiveCocoa

There are two APIs that uses the command pattern. As @sharplet said, these might have been brought over from ReactiveObjC, e.g. button.rac_command.

extension Reactive where Host: UIButton {
    public var pressed: MutableProperty<CocoaAction> { ... }
}

extension Reactive where Host: UIBarButtonItem {
    public var pressed: MutableProperty<CocoaAction> { ... }
}

It just feels wrong to have the view owning the Action with a strong reference. The ownership of the Action (indirectly via CocoaAction) should be left to its originated layer (be it VC or VM).

Therefore, after a lengthy discussion with @liscio and @sharplet, it is proposed to have a new interface specifically crafted for Action connecting with UIControl and NSControls. The concept is like:

extension Reactive where Base: UIControl {
    /// Create a trigger signal for a specific set of control events.
    public func trigger(for controlEvents: UIControlEvents) -> Signal<(), NoError>

    /// The action is weakly referenced internally.
    public func setAction<Input, Output, Error>(_ action: Action<Input, Output, Error>, for controlEvents: UIControlEvents, inputTransform: (Self, UIControlEvents) -> Input)

    public func removeAction()

    /// ... some convenience overloads of `setAction(_:for:inputTransform:)`.
}

With this API, we would remove all Action binding targets from the UIControl and NSControl subclasses, since the API should have covered all of them.

In a greater sense, we would be mimicking the target-action pattern behind @IBAction and @IBOutlet.

In the Interface Builder, we may bind a _Sent Action_ of a UIButton to a _Received Action_ (an @IBAction method) of a UIViewController, and the UIButton sends a message to the weakly referenced target.

In our case, we would expose a collection of event signals on the controls. With Actions as binding targets, Action may have a similar role to @IBAction. For example:

// Received Action <~ Sent Action
commitAction <~ confirmButton.touchUpInside

// The reverse `isEnabled` binding is now explicit & optional.
confirmButton.isEnabled <~ commitAction.isEnabled

// OR

confirmButton.setAction(commitAction, for: .touchUpInside)

which are equivalents of

confirmButton.rac_pressed.value = CocoaAction(commitAction)

but with a decoupled ownership via our weak-to-weak binding semantics.

enhancement

Most helpful comment

Gonna hide it then. Keep it simple.

All 22 comments

Don't forget we'd also need this for equivalent functionality:

confirmButton.rac.isEnabled <~ commitAction.isEnabled

Hmm, I overlooked that. It seems we should go all-in with Action then.

@andersio I'm not sure what you mean by that, and why it resulted in this issue getting closed.

To address what @sharplet brings up, I think it's fine to separate the propagation of values to the isEnabled inlet on a button.

Hiding that fact in a convenience method/pattern isn't buying us much, in my opinion.

It just feels wrong to have the view owning the Action with a strong reference. The ownership of the Action (indirectly via CocoaAction) should be left to its originated layer (be it VC or VM).

I don't think I agree. Why does that feel wrong to you?

The weak referencing is a tiny bits of the proposed API, which pushes a single set of UIControl methods that covers all controls using the command pattern (via Action).

In Rex, it was exclusive to UIButton with a specific implementation for it.

AFAIK, an Action usually represents a command, is owned by a view model, and likely has both internal and external dependencies from/on its state. Like the Cocoa target-action pattern, I don't see why it should be a strong reference when it has an apparent owner.

The Rex API using a strong reference is likely an artefact of how CocoaAction behaves, which is required to be retained so that values can be propagated to the wrapped Action.

The proposed API here instead takes an Action directly.

Rex 0.12:

extension UIButton {
    public var rex_pressed: MutableProperty<CocoaAction> { get }

    // This one would become `BindingTarget`. Irrelevant to this issue.
    public var rex_title: MutableProperty<String> { get }
}

Proposed (Works on all UIControls):

extension Reactive where Base: UIControl {
    /// The action is weakly referenced internally.
    public func setAction<Input, Output, Error>(
        _ action: Action<Input, Output, Error>,
        for controlEvents: UIControlEvents,
        inputTransform: (Self, UIControlEvents) -> Input
    )

    public func removeAction()

    /// ... some convenience overloads of `setAction(_:for:inputTransform:)`.
}

Generally speaking, we have three classes of UI states + one specifically for the command pattern with Action:

  1. Mutable keys that has no means or values to be observed.
    e.g. UIControl.isEnabled, UILabel.text, UIView.isHidden.

Model as BindingTargets.

  1. Control events, and notifications of method calls.
    e.g. UIControl.trigger(for: .valueChanged), UITableViewCell.trigger(for: #selector(prepareForReuse))

Model as Signals.

  1. Mutable keys + corresponding control event that posts upon user interactions.
    e.g. UITextField.text, NSTextField.text, UISwitch.isOn

#3229

  1. [_Associating Action with a control. (control event observation + binding of enabling state)_]
    (This is just a convenience pattern built on top of [1] and [2].)
    e.g. UIButton.rac_command

_This issue._

@andersio I think that unfortunately removeAction() will have to also specify what control events you're removing the action for, _or_ just for 1.0 I'd rename it removeAllActions() and add a note to setAction() to indicate that the control currently only handles a single action being hooked using this "easy API" since that's how 99.9% of people will use it, anyway.

It is debatable. IMO multiple actions might not make sense, since in this case multiple actions can manipulate isEnabled concurrently (= kinda indeterministic).

Why not add (Swift) properties that directly set the given action?

extension Reactive where Base: UIButton {
    var pressed: CocoaAction {
        get { … }
        set { … }
    } 
}

Or maybe that was included in your some convenience overloads section?

I think it's fine if we want to add a generic API, but I think the convenience APIs are more important. That's the API that I'd show off to the world. I think something like this is very compelling:

button.reactive.pressed = CocoaAction(viewModel.action)

As long as we're going with setAction(…), removing should probably be done by setting a nil action. No need for a separate API.

This undoubtedly looks better, yeah. These should definitely be kept. I'd still propose to make CocoaAction have a weak reference to the Action though.

setAction is updated (790b3e8), and pressed is back (790b3e8).

extension Reactive where Base: UIButton {
    public var pressed: CocoaAction<Base> { get nonmutating set }
}

extension Reactive where Base: UIControl {
    public var action: CocoaAction<Base>? { get }
    public var controlEventsForAction: UIControlEvents? { get }

    /// `CocoaAction` is retained by `self`, but `CocoaAction` weakly references `Action`.
    public func setAction(
        _ action: CocoaAction<Base>?,
        for controlEvents: UIControlEvents
    )
}

I think that API would need to support multiple actions. So this should be valid:

button.reactive.setAction(action1, for: .touchUpInside)
button.reactive.setAction(action2, for: .touchUpOutside)
// button now has 2 actions set for 2 different events

Why does CocoaAction weakly reference Action?

@mdiep

// button now has 2 actions set for 2 different events

That would mean the enabling state of button is bound to both action1.isEnabled and action2.isEnabled. I am not sure if this is what we want. How should the enabling state be determined? An AND of all Actions' enabling state?

Why does CocoaAction weakly reference Action?

Reverted it. I'd admit that it doesn't cause trouble for now (and during the history of Rex).

That would mean the enabling state of button is bound to both action1.isEnabled and action2.isEnabled. I am not sure if this is what we want. How should the enabling state be determined? An AND of all Actions' enabling state?

AND doesn't seem right to me.

Here are our options:

  1. Give up on enabling completely
  2. AND all actions' enabled state
  3. OR all actions' enabled state
  4. Enable iff there's exactly 1 action
  5. Enable in .pressed, etc. and not in setAction(:for:) directly

(5) Seems like the best solution to me. The common case is easy and convenient. If you want to do something more complex, you're still able to.

(5) means setAction would have no difference from doing multiple actionN <~ reactive.trigger(for: .specificEvent). Moreover, what should happen if a control contains multiple convenience command slots?

It's still different in that:

  1. Cancelation is different
  2. It only allows one action per event
  3. It passes the control as the input to the CocoaAction

Otherwise they're roughly the same, yes. But I think that's okay.

If the plan is to support more than one action, we should probably rename it to addAction.

Let's say if we add a propagateEnablingState argument to setAction, we still have to decide how to handle the vector of isEnabled. (Configurable? e.g. and/or/undefined?)

I'm not sure it's worth supporting >1 action in the "convenience property" that's being discussed here, as we've left the alternate usage (individually triggering actions with trigger(for:)) for folks that need more than one.

I can't see a good reason to add more than one action to a button in this manner given the easy-to-use alternative (that also happens to match the usage of our other Signal/BindingTarget stuff.)

Basically, if you want a "simple API", you use the action property as in here. Its enabled state is reflected in the button.

If you want to attach a button to >1 action, then you use their individual triggers, and then hook up a map of your own to hook into the isEnabled BindingTarget. Does that sound reasonable?

Basically, if you want a "simple API", you use the action property as in here. Its enabled state is reflected in the button.

If you want to attach a button to >1 action, then you use their individual triggers, and then hook up a map of your own to hook into the isEnabled BindingTarget. Does that sound reasonable?

That sounds reasonable to me.

So do we need setAction(:for:) at all then?

Gonna hide it then. Keep it simple.

Was this page helpful?
0 / 5 - 0 ratings