web-dev-qa-db-fra.com

Accrocher UIButton à la fermeture? (Rapide, action-cible)

Je souhaite connecter un UIButton à un morceau de code. D'après ce que j'ai trouvé, la méthode recommandée pour le faire dans Swift consiste à utiliser la fonction addTarget(target: AnyObject?, action: Selector, forControlEvents: UIControlEvents). Ceci utilise vraisemblablement la construction Selector pour des raisons de compatibilité avec les bibliothèques Obj-C. Je pense que je comprends la raison de @selector dans Obj-C - être capable de faire référence à une méthode car dans Obj-C, les méthodes ne sont pas des valeurs de première classe.

Dans Swift cependant, les fonctions sont des valeurs de première classe. Y a-t-il un moyen de connecter un UIButton à une fermeture, quelque chose de similaire à ceci:

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

À ma connaissance, ce qui précède n'est actuellement pas possible. Considérant que Swift semble vouloir être assez fonctionnel, pourquoi? Les deux options pourraient clairement coexister pour une compatibilité ascendante. Pourquoi cela ne fonctionne-t-il pas plus comme onClick () dans JS? Il semble que la méthode uniquement pour connecter un UIButton à une paire cible-action consiste à utiliser quelque chose qui existe uniquement pour des raisons de compatibilité avec les versions antérieures (Selector).

Mon cas d'utilisation est de créer des boutons UIB dans une boucle pour différents objets, puis de les associer à une fermeture. (Définir une balise/rechercher dans un dictionnaire/sous-classer UIButton sont des solutions semi-sales, mais je suis intéressé par la façon de procéder de manière fonctionnelle, c'est-à-dire cette approche de fermeture)

33
rafalio

Vous pouvez remplacer l'action cible par une fermeture en ajoutant un wrapper de fermeture d'assistance (ClosureSleeve) et en l'ajoutant en tant qu'objet associé au contrôle afin qu'il soit conservé.

C'est une solution similaire à celle de la réponse de n13 (en haut). Mais je le trouve plus simple et plus élégant. La fermeture est invoquée plus directement et l'encapsuleur est automatiquement conservé (ajouté en tant qu'objet associé).

Swift 3 et 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)
    }
}

Usage:

button.addAction {
    print("Hello")
}

Il se connecte automatiquement à l'événement .primaryActionTriggered, ce qui correspond à .touchUpInside pour UIButton.

21
Marián Černý

L’approche générale pour tout ce qui, selon vous, devrait être dans les bibliothèques mais n’est pas: écrire une catégorie. Il y en a beaucoup sur GitHub mais je n'en ai pas trouvé dans Swift, alors j'ai écrit le mien:

=== Mettez ceci dans son propre fichier, comme 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)
    }
}

Puis invoquez-le comme ceci:

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

    // your code here

}

Clairement, cela pourrait être amélioré, il pourrait y avoir des options pour les différents types d’événements (pas seulement des retouches à l’intérieur), etc. Mais cela a fonctionné pour moi… .. C'est légèrement plus compliqué que la version pure d'ObjC en raison de la nécessité d'un wrapper pour le bloc. Le compilateur Swift ne permet pas de stocker le bloc en tant que "AnyObject". Alors je l'ai juste emballé.

19
n13

Ce n'est pas nécessairement un "accrochage", mais vous pouvez effectivement obtenir ce problème en sous-classant 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)
    }
}

Utilisation:

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")
}
7
Jacob Caraballo

Ceci est facilement résolu en utilisant RxSwift

import RxSwift
import RxCocoa

...

@IBOutlet weak var button:UIButton!

...

let taps = button.rx.tap.asDriver() 

taps.drive(onNext: {
    // handle tap
})

Modifier

Je voulais reconnaître que RxSwift/RxCocoa est une dépendance assez lourde à ajouter à un projet simplement pour résoudre cette exigence. Il peut y avoir des solutions plus légères disponibles ou juste rester avec le motif cible/action. 

Dans tous les cas, si l’idée d’une approche déclarative générale pour le traitement des événements liés aux applications et aux utilisateurs vous intéresse, donnez définitivement un aperçu de RxSwift. C'est la bombe.

6
David James

Selon la solution de n13 , j'ai réalisé une version de Swift3.

J'espère que cela pourrait aider certaines personnes comme moi.

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)
    }
}
5
Steven Jiang

Vous pouvez l'aborder avec une classe proxy qui achemine les événements via le mécanisme cible/action (sélecteur) à une fermeture de votre création. Je l'ai fait pour les reconnaisseurs de geste, mais le même schéma devrait être utilisé pour les contrôles.

Vous pouvez faire quelque chose comme ça:

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
}

Le seul inconvénient est que, puisque addTarget: ... ne conserve pas la cible, vous devez conserver les objets de répartition (comme pour le tableau de foncteurs). Bien sûr, vous n’avez pas besoin de vous en tenir aux boutons, vous pouvez le faire via une référence capturée dans la fermeture, mais vous souhaiterez probablement des références explicites.

PS. J'ai essayé de tester cela sur un terrain de jeu, mais je ne pouvais pas faire fonctionner sendActionsForControlEvents. J'ai cependant utilisé cette approche pour reconnaître les gestes.

1
Chris Conover

UIButton hérite de UIControl, qui gère la réception des entrées et leur transmission à la sélection. Selon la documentation, l'action est "Un sélecteur identifiant un message d'action. Il ne peut pas être NULL". Et un sélecteur est strictement un pointeur sur une méthode. 

Je pense que compte tenu des priorités de Swift, cela semble possible, mais cela ne semble pas être le cas. 

1
Jonah Katz

Les objets relatedObject et wrapper, ainsi que les pointeurs et importations d'ObjectiveC ne sont pas nécessaires, du moins dans Swift 3. Cela fonctionne très bien et est beaucoup plus rapide. N'hésitez pas à ajouter un typealias ici pour () -> () si vous le trouvez plus lisible, mais je trouve plus facile de lire directement la signature de bloc.

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?()
    }
} 
0
Nate Birkholz