Reactivecocoa: Control de enlaces para `Action`s.

Creado en 4 oct. 2016  ·  22Comentarios  ·  Fuente: ReactiveCocoa/ReactiveCocoa

Hay dos API que utilizan el patrón de comando. Como dijo @sharplet , estos podrían haber sido traídos de ReactiveObjC, por ejemplo, button.rac_command .

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

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

Simplemente se siente mal tener la vista poseyendo el Action con una fuerte referencia. La propiedad de Action (indirectamente a través de CocoaAction ) debe dejarse en su capa de origen (ya sea VC o VM).

Por lo tanto, después de una larga discusión con @liscio y @sharplet , se propone tener una nueva interfaz diseñada específicamente para Action conecte con UIControl y NSControl s. El concepto es como:

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:)`.
}

Con esta API, eliminaríamos todos los objetivos de enlace Action de las subclases UIControl y NSControl, ya que la API debería haberlos cubierto todos.

En un sentido más amplio, estaríamos imitando el patrón de acción objetivo detrás de @IBAction y @IBOutlet .

En el Interface Builder, podemos vincular una _Acción enviada_ de un UIButton a una _Received Action_ (un método @IBAction ) de un UIViewController, y el UIButton envía un mensaje al objetivo referenciado débilmente.

En nuestro caso, expondríamos una colección de señales de eventos en los controles. Con Action s como objetivos vinculantes, Action puede tener una función similar a @IBAction . Por ejemplo:

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

que son equivalentes de

confirmButton.rac_pressed.value = CocoaAction(commitAction)

pero con una propiedad disociada a través de nuestra semántica de enlace débil a débil.

enhancement

Comentario más útil

Lo esconderé entonces. Mantenlo simple.

Todos 22 comentarios

No olvide que también necesitaríamos esto para una funcionalidad equivalente:

confirmButton.rac.isEnabled <~ commitAction.isEnabled

Hmm, lo pasé por alto. Parece que deberíamos ir con Action entonces.

@andersio No estoy seguro de lo que quiere decir con eso, y por qué resultó en que este problema se cerrara.

Para abordar lo que menciona @sharplet , creo que está bien separar la propagación de valores a la entrada isEnabled en un botón.

Ocultar ese hecho en un método / patrón de conveniencia no nos compra mucho, en mi opinión.

Simplemente se siente mal tener la vista que posee la Acción con una fuerte referencia. La propiedad de la acción (indirectamente a través de CocoaAction) debe dejarse a su capa de origen (ya sea VC o VM).

No creo que esté de acuerdo. ¿Por qué te parece mal eso?

La referencia débil es una pequeña parte de la API propuesta, que empuja un solo conjunto de métodos UIControl que cubre todos los controles usando el patrón de comando (a través de Action ).

En Rex, era exclusivo de UIButton con una implementación específica para él.

AFAIK, un Action generalmente representa un comando, es propiedad de un modelo de vista y probablemente tiene dependencias internas y externas de / en su estado. Al igual que el patrón de acción objetivo de Cocoa, no veo por qué debería ser una referencia sólida cuando tiene un propietario aparente.

La API de Rex que usa una referencia sólida es probablemente un artefacto de cómo se comporta CocoaAction , que se debe retener para que los valores se puedan propagar al Action envuelto.

La API propuesta aquí, en cambio, toma Action directamente.

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

Propuesto (funciona en todos los 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:)`.
}

En términos generales, tenemos tres clases de estados de IU + uno específicamente para el patrón de comando con Action :

  1. Claves mutables que no tienen medios ni valores que observar.
    por ejemplo, UIControl.isEnabled , UILabel.text , UIView.isHidden .

Modele como BindingTarget s.

  1. Controle eventos y notificaciones de llamadas a métodos.
    por ejemplo, UIControl.trigger(for: .valueChanged) , UITableViewCell.trigger(for: #selector(prepareForReuse))

Modele como Signal s.

  1. Teclas mutables + evento de control correspondiente que se publica en las interacciones del usuario.
    por ejemplo, UITextField.text , NSTextField.text , UISwitch.isOn

N.º 3229

  1. [ _Asociar Action con un control.
    (Este es solo un patrón de conveniencia construido sobre [1] y [2]).
    por ejemplo, UIButton.rac_command

_Este problema. _

@andersio Creo que desafortunadamente removeAction() también tendrá que especificar para qué eventos de control está eliminando la acción, _o_ solo para 1.0 lo cambiaría el nombre removeAllActions() y agregaría una nota a setAction() para indicar que el control actualmente solo maneja una sola acción que se engancha usando esta "API fácil", ya que así es como el 99.9% de las personas lo usarán, de todos modos.

Es discutible. En mi opinión, las acciones múltiples pueden no tener sentido, ya que en este caso las acciones múltiples pueden manipular isEnabled mismo tiempo (= algo indeterminista).

¿Por qué no agregar propiedades (Swift) que establezcan directamente la acción dada?

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

¿O quizás eso estaba incluido en tu sección some convenience overloads ?

Creo que está bien si queremos agregar una API genérica, pero creo que las API de conveniencia son más importantes. Esa es la API que le mostraría al mundo. Creo que algo como esto es muy convincente:

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

Siempre que vayamos con setAction(…) , la eliminación probablemente debería realizarse configurando una acción nil . No se necesita una API separada.

Sin duda, esto se ve mejor, sí. Definitivamente deben conservarse. Sin embargo, todavía propondría hacer que CocoaAction tenga una referencia débil al Action .

setAction se actualiza ( 790b3e8 ) y pressed está de vuelta ( 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
    )
}

Creo que la API debería admitir múltiples acciones. Entonces esto debería ser válido:

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

¿Por qué CocoaAction referencia débilmente a Action ?

@mdiep

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

Eso significaría que el estado habilitante de button está vinculado tanto a action1.isEnabled como a action2.isEnabled . No estoy seguro de si esto es lo que queremos. ¿Cómo se debe determinar el estado habilitante? ¿Un AND de todos los Action s 'estado habilitante?

¿Por qué CocoaAction hace referencia débil a Action?

Lo revirtió. Admito que no causa problemas por ahora (y durante la historia de Rex).

Eso significaría que el estado de habilitación del botón está vinculado tanto a action1.isEnabled como a action2.isEnabled. No estoy seguro de si esto es lo que queremos. ¿Cómo se debe determinar el estado habilitante? ¿Un estado habilitante de AND de todas las acciones?

Y no me parece correcto.

Aquí están nuestras opciones:

  1. Renunciar por completo a la habilitación
  2. Y el estado habilitado de todas las acciones
  3. O el estado habilitado de todas las acciones
  4. Habilite si hay exactamente 1 acción
  5. Habilitar en .pressed , etc. y no en setAction(:for:) directamente

(5) Me parece la mejor solución. El caso común es fácil y conveniente. Si desea hacer algo más complejo, aún puede hacerlo.

(5) significa que setAction no tendría ninguna diferencia con hacer varios actionN <~ reactive.trigger(for: .specificEvent) . Además, ¿qué debería suceder si un control contiene múltiples ranuras de comando de conveniencia?

Todavía es diferente en eso:

  1. La cancelación es diferente
  2. Solo permite una acción por evento.
  3. Pasa el control como entrada al CocoaAction

De lo contrario, son más o menos iguales, sí. Pero creo que está bien.

Si el plan es admitir más de una acción, probablemente deberíamos cambiarle el nombre a addAction .

Digamos que si agregamos un argumento propagateEnablingState a setAction , todavía tenemos que decidir cómo manejar el vector de isEnabled . (¿Configurable, por ejemplo, y / o indefinido?)

No estoy seguro de que valga la pena admitir> 1 acción en la "propiedad de conveniencia" que se está discutiendo aquí, ya que hemos dejado el uso alternativo (acciones de activación individual con trigger(for:) ) para las personas que necesitan más de una.

No veo una buena razón para agregar más de una acción a un botón de esta manera dada la alternativa fácil de usar (que también coincide con el uso de nuestras otras cosas de Signal / BindingTarget).

Básicamente, si desea una "API simple", utilice la propiedad action como aquí. Su estado habilitado se refleja en el botón.

Si desea adjuntar un botón a> 1 acción, utilice sus trigger s individuales, y luego conecte un map propio para conectarlo al isEnabled BindingTarget . ¿Eso suena razonable?

Básicamente, si desea una "API simple", use la propiedad de acción como aquí. Su estado habilitado se refleja en el botón.

Si desea adjuntar un botón a> 1 acción, utilice sus activadores individuales y luego conecte un mapa propio para conectarse al isEnabled BindingTarget. ¿Eso suena razonable?

Eso suena razonable para mí.

Entonces, ¿necesitamos setAction(:for:) en absoluto?

Lo esconderé entonces. Mantenlo simple.

¿Fue útil esta página
0 / 5 - 0 calificaciones