the programming logo lenguaje language apple ios swift

ios - programming - ¿Conectando UIButton al cierre?(Swift, acción objetivo)



the swift programming language pdf (8)

Quiero conectar un UIButton a un fragmento de código; por lo que he encontrado, el método preferido para hacer esto en Swift es usar la función addTarget(target: AnyObject?, action: Selector, forControlEvents: UIControlEvents) . Esto usa la construcción Selector presumiblemente para compatibilidad con las bibliotecas Obj-C. Creo que entiendo la razón de @selector en Obj-C: poder referirme a un método, ya que en Obj-C los métodos no son valores de primera clase.

Sin embargo, en Swift, las funciones son valores de primera clase. ¿Hay una manera de conectar un UIButton a un cierre, algo similar a esto:

// -- Some code here that sets up an object X let buttonForObjectX = UIButton() // -- configure properties here of the button in regards to object // -- for example title buttonForObjectX.addAction(action: {() in // this button is bound to object X, so do stuff relevant to X }, forControlEvents: UIControlEvents.TouchUpOutside)

Que yo sepa, lo anterior no es posible actualmente. Teniendo en cuenta que Swift parece que pretende ser bastante funcional, ¿por qué es esto? Las dos opciones podrían coexistir claramente por compatibilidad con versiones anteriores. ¿Por qué esto no funciona más como onClick () en JS? Parece que la única forma de conectar un UIButton a un par objetivo-acción es usar algo que existe únicamente por razones de compatibilidad con versiones anteriores ( Selector ).

Mi caso de uso es crear UIButtons en un bucle para diferentes objetos, y luego conectar cada uno a un cierre. (Establecer una etiqueta / buscar en un diccionario / subclase UIButton son semi soluciones sucias, pero estoy interesado en cómo hacer esto funcionalmente, es decir, este enfoque de cierre)


De acuerdo con la solución de n13 , hice una versión swift3.

Espera que pueda ayudar a algunas personas como yo.

import Foundation import UIKit import ObjectiveC var ActionBlockKey: UInt8 = 0 // a type for our action block closure typealias BlockButtonActionBlock = (_ sender: UIButton) -> Void class ActionBlockWrapper : NSObject { var block : BlockButtonActionBlock init(block: @escaping BlockButtonActionBlock) { self.block = block } } extension UIButton { func block_setAction(block: @escaping BlockButtonActionBlock, for control: UIControlEvents) { objc_setAssociatedObject(self, &ActionBlockKey, ActionBlockWrapper(block: block), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) self.addTarget(self, action: #selector(UIButton.block_handleAction), for: .touchUpInside) } func block_handleAction(sender: UIButton, for control:UIControlEvents) { let wrapper = objc_getAssociatedObject(self, &ActionBlockKey) as! ActionBlockWrapper wrapper.block(sender) } }


El enfoque general para cualquier cosa que creas que debería estar en las bibliotecas pero no lo es: escribe una categoría. Hay muchos de este en particular en GitHub, pero no encontré ninguno en Swift, así que escribí el mío:

=== Pon esto en su propio archivo, como UIButton + Block.swift ===

import ObjectiveC var ActionBlockKey: UInt8 = 0 // a type for our action block closure typealias BlockButtonActionBlock = (sender: UIButton) -> Void class ActionBlockWrapper : NSObject { var block : BlockButtonActionBlock init(block: BlockButtonActionBlock) { self.block = block } } extension UIButton { func block_setAction(block: BlockButtonActionBlock) { objc_setAssociatedObject(self, &ActionBlockKey, ActionBlockWrapper(block: block), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) addTarget(self, action: "block_handleAction:", forControlEvents: .TouchUpInside) } func block_handleAction(sender: UIButton) { let wrapper = objc_getAssociatedObject(self, &ActionBlockKey) as! ActionBlockWrapper wrapper.block(sender: sender) } }

Entonces invócalo así:

myButton.block_setAction { sender in // if you''re referencing self, use [unowned self] above to prevent // a retain cycle // your code here }

Claramente, esto podría mejorarse, podría haber opciones para los diversos tipos de eventos (no solo retoque interno) y así sucesivamente. Pero esto funcionó para mí. Es un poco más complicado que la versión ObjC pura debido a la necesidad de una envoltura para el bloque. El compilador Swift no permite almacenar el bloque como "AnyObject". Así que acabo de envolverlo.


El objeto asociado y el ajuste, los punteros y la importación de ObjectiveC no son necesarios, al menos en Swift 3. Esto funciona muy bien y es mucho más rápido. Siéntase libre de lanzar una tipografía en allí para () -> () si le resulta más legible, pero me resulta más fácil leer la firma de bloque directamente.

import UIKit class BlockButton: UIButton { fileprivate var onAction: (() -> ())? func addClosure(_ closure: @escaping () -> (), for control: UIControlEvents) { self.addTarget(self, action: #selector(actionHandler), for: control) self.onAction = closure } dynamic fileprivate func actionHandler() { onAction?() } }


Esto no es necesariamente un "enganche", pero puede lograr este comportamiento de manera efectiva subclasificando UIButton:

class ActionButton: UIButton { var touchDown: ((button: UIButton) -> ())? var touchExit: ((button: UIButton) -> ())? var touchUp: ((button: UIButton) -> ())? required init?(coder aDecoder: NSCoder) { fatalError("init(coder:)") } override init(frame: CGRect) { super.init(frame: frame) setupButton() } func setupButton() { //this is my most common setup, but you can customize to your liking addTarget(self, action: #selector(touchDown(_:)), forControlEvents: [.TouchDown, .TouchDragEnter]) addTarget(self, action: #selector(touchExit(_:)), forControlEvents: [.TouchCancel, .TouchDragExit]) addTarget(self, action: #selector(touchUp(_:)), forControlEvents: [.TouchUpInside]) } //actions func touchDown(sender: UIButton) { touchDown?(button: sender) } func touchExit(sender: UIButton) { touchExit?(button: sender) } func touchUp(sender: UIButton) { touchUp?(button: sender) } }

Utilizar:

let button = ActionButton(frame: buttonRect) button.touchDown = { button in print("Touch Down") } button.touchExit = { button in print("Touch Exit") } button.touchUp = { button in print("Touch Up") }


Esto se resuelve fácilmente usando RxSwift

import RxSwift import RxCocoa ... @IBOutlet weak var button:UIButton! ... let taps = button.rx.tap.asDriver() taps.drive(onNext: { // handle tap })

Editar :

Quería reconocer que RxSwift / RxCocoa es una dependencia bastante pesada para agregar a un proyecto solo para resolver este requisito. Puede haber soluciones más ligeras disponibles o simplemente seguir el patrón de objetivo / acción.

En cualquier caso, si la idea de un enfoque declarativo de propósito general para el manejo de la aplicación y los eventos del usuario le atrae, definitivamente dele un vistazo a RxSwift. Es la bomba.


Puede abordar esto con una clase de proxy que enrute los eventos a través del mecanismo de destino / acción (selector) hasta el cierre de su creación. He hecho esto para los reconocedores de gestos, pero el mismo patrón debería ser válido para los controles.

Podrías hacer algo como esto:

import UIKit @objc class ClosureDispatch { init(f:()->()) { self.action = f } func execute() -> () { action() } let action: () -> () } var redBlueGreen:[String] = ["Red", "Blue", "Green"] let buttons:[UIButton] = map(0..<redBlueGreen.count) { i in let text = redBlueGreen[i] var btn = UIButton(frame: CGRect(x: i * 50, y: 0, width: 100, height: 44)) btn.setTitle(text, forState: .Normal) btn.setTitleColor(UIColor.redColor(), forState: .Normal) btn.backgroundColor = UIColor.lightGrayColor() return btn } let functors:[ClosureDispatch] = map(buttons) { btn in let functor = ClosureDispatch(f:{ [unowned btn] in println("Hello from /(btn.titleLabel!.text!)") }) btn.addTarget(functor, action: "execute", forControlEvents: .TouchUpInside) return functor }

La única advertencia de esto, es que dado que addTarget: ... no retiene el objetivo, debe aferrarse a los objetos de despacho (como se hizo con la matriz de functores). No es estrictamente necesario aferrarse a los botones, por supuesto, ya que podría hacerlo a través de una referencia capturada en el cierre, pero es probable que desee referencias explícitas.

PD. Traté de probar esto en el patio de recreo, pero no pude hacer que sendActionsForControlEvents funcionara. Sin embargo, he utilizado este enfoque para los reconocedores de gestos.


Puede reemplazar la acción de destino con un cierre agregando un envoltorio de cierre de ayudante (ClosureSleeve) y agregándolo como un objeto asociado al control para que se retenga.

Esta es una solución similar a la que está en la respuesta de n13 (arriba). Pero me parece más simple y más elegante. El cierre se invoca más directamente y el contenedor se retiene automáticamente (se agrega como un objeto asociado).

Swift 3 y 4

class ClosureSleeve { let closure: () -> () init(attachTo: AnyObject, closure: @escaping () -> ()) { self.closure = closure objc_setAssociatedObject(attachTo, "[/(arc4random())]", self, .OBJC_ASSOCIATION_RETAIN) } @objc func invoke() { closure() } } extension UIControl { func addAction(for controlEvents: UIControlEvents = .primaryActionTriggered, action: @escaping () -> ()) { let sleeve = ClosureSleeve(attachTo: self, closure: action) addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents) } }

Uso:

button.addAction { print("Hello") }

Se engancha automáticamente al evento .primaryActionTriggered que equivale a .touchUpInside para UIButton.


UIButton hereda de UIControl, que maneja la recepción de entrada y reenvío a la selección. Según los documentos, la acción es "Un selector que identifica un mensaje de acción. No puede ser NULO". Y un Selector es estrictamente un puntero a un método.

Creo que dado el énfasis que Swift parece estar poniendo en Closures, esto sería posible, pero este no parece ser el caso.