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 NSControl
s. 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 Action
s 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.
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.
Implemented in https://github.com/ReactiveCocoa/ReactiveCocoa/pull/3210/commits/f983a746f915a9a517e9e076c19581724075c562. Let's see how others think. 😆
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 UIControl
s):
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
:
UIControl.isEnabled
, UILabel.text
, UIView.isHidden
.Model as BindingTarget
s.
UIControl.trigger(for: .valueChanged)
, UITableViewCell.trigger(for: #selector(prepareForReuse))
Model as Signal
s.
UITextField.text
, NSTextField.text
, UISwitch.isOn
#3229
Action
with a control. (control event observation + binding of enabling state)_]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 Action
s' 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:
.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:
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 trigger
s, 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.
Most helpful comment
Gonna hide it then. Keep it simple.