web-dev-qa-db-fra.com

Ajout d'une fermeture en tant que cible à un UIButton

J'ai une classe de contrôle générique qui doit définir l'achèvement du bouton en fonction du contrôleur de vue. En raison de cette fonction setLeftButtonActionWithClosure doit prendre comme paramètre une fermeture qui devrait être définie comme action sur un unbutton.Comment serait-il possible dans Swift puisque nous devons passer le nom de la fonction sous forme de chaîne à action: paramètre.

func setLeftButtonActionWithClosure(completion: () -> Void)
{
self.leftButton.addTarget(<#target: AnyObject?#>, action: <#Selector#>, forControlEvents: <#UIControlEvents#>)
}
28
Ilker Baltaci

NOTE: Comme @ EthanHuang a déclaré "Cette solution ne fonctionne pas si vous avez plus de deux instances. Toutes les actions seront écrasées par la dernière affectation." N'oubliez pas ceci lorsque vous développerez, je publierai une autre solution bientôt.

Si vous souhaitez ajouter une fermeture en tant que cible à une UIButton, vous devez ajouter une fonction à la classe UIButton à l'aide de extension.

import UIKit

extension UIButton {
    private func actionHandleBlock(action:(() -> Void)? = nil) {
        struct __ {
            static var action :(() -> Void)?
        }
        if action != nil {
            __.action = action
        } else {
            __.action?()
        }
    }

    @objc private func triggerActionHandleBlock() {
        self.actionHandleBlock()
    }

    func actionHandle(controlEvents control :UIControlEvents, ForAction action:() -> Void) {
        self.actionHandleBlock(action)
        self.addTarget(self, action: "triggerActionHandleBlock", forControlEvents: control)
    }
}

et l'appel:

 let button = UIButton()
 button.actionHandle(controlEvents: UIControlEvents.TouchUpInside, 
 ForAction:{() -> Void in
     print("Touch")
 })
21
Armanoide

Solution similaire à celles déjà répertoriées, mais peut-être plus légère:

class ClosureSleeve {
    let closure: ()->()

    init (_ closure: @escaping ()->()) {
        self.closure = closure
    }

    @objc func invoke () {
        closure()
    }
}

extension UIControl {
    func addAction(for controlEvents: UIControl.Event, _ closure: @escaping ()->()) {
        let sleeve = ClosureSleeve(closure)
        addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
        objc_setAssociatedObject(self, String(format: "[%d]", arc4random()), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
    }
}

Usage:

button.addAction(for: .touchUpInside) {
    print("Hello, Closure!")
}

Ou si vous évitez de conserver des boucles:

self.button.addAction(for: .touchUpInside) { [weak self] in
    self?.doStuff()
}

L'utilisateur MH175 mentionne qu'il y aura une exception d'exécution s'il utilise la propriété "allTargets du contrôle: static Set_unnconditionalBridgeFromObjectiveC (_ :) -". Faire en sorte que ClosureSleeve soit étendu à partir de NSObject résoudra le problème:

@objc class ClosureSleeve: NSObject {
    let closure: ()->()

    init (_ closure: @escaping ()->()) {
        self.closure = closure
        super.init()
    }

    @objc func invoke () {
        closure()
    }
}
70
aepryus

Vous pouvez y parvenir efficacement 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")
}
10
Jacob Caraballo

C'est fondamentalement la réponse d'Armanoide , ci-dessus, mais avec quelques légères modifications qui me sont utiles:

  • la fermeture transmise peut prendre un argument UIButton, vous permettant de passer self
  • les fonctions et les arguments sont renommés de manière à clarifier ce qui se passe, par exemple, en distinguant une fermeture Swift d'une action UIButton.

    private func setOrTriggerClosure(closure:((button:UIButton) -> Void)? = nil) {
    
      //struct to keep track of current closure
      struct __ {
        static var closure :((button:UIButton) -> Void)?
      }
    
      //if closure has been passed in, set the struct to use it
      if closure != nil {
        __.closure = closure
      } else {
        //otherwise trigger the closure
        __. closure?(button: self)
      }
    }
    @objc private func triggerActionClosure() {
      self.setOrTriggerClosure()
    }
    func setActionTo(closure:(UIButton) -> Void, forEvents :UIControlEvents) {
      self.setOrTriggerClosure(closure)
      self.addTarget(self, action:
        #selector(UIButton.triggerActionClosure),
                     forControlEvents: forEvents)
    }
    

Beaucoup d'accessoires à Armanoide cependant pour une certaine magie robuste ici.

3
Le Mot Juiced

J'ai commencé à utiliser la réponse de Armanoide sans tenir compte du fait qu'elle sera annulée par la deuxième mission, principalement parce qu'au début, j'en avais besoin d'un endroit précis qui importait peu. Mais il a commencé à s'effondrer.

J'ai proposé une nouvelle implémentation en utilisant AssicatedObjects qui n'a pas cette limitation, je pense une syntaxe plus intelligente, mais ce n'est pas une solution complète:

C'est ici:

typealias ButtonAction = () -> Void

fileprivate struct AssociatedKeys {
  static var touchUp = "touchUp"
}

fileprivate class ClosureWrapper {
  var closure: ButtonAction?

  init(_ closure: ButtonAction?) {
    self.closure = closure
  }
}

extension UIControl {

  @objc private func performTouchUp() {

    guard let action = touchUp else {
      return
    }

    action()

  }

  var touchUp: ButtonAction? {

    get {

      let closure = objc_getAssociatedObject(self, &AssociatedKeys.touchUp)
      guard let action = closure as? ClosureWrapper else{
        return nil
      }
      return action.closure
    }

    set {
      if let action = newValue {
        let closure = ClosureWrapper(action)
        objc_setAssociatedObject(
          self,
          &AssociatedKeys.touchUp,
          closure as ClosureWrapper,
          .OBJC_ASSOCIATION_RETAIN_NONATOMIC
        )
        self.addTarget(self, action: #selector(performTouchUp), for: .touchUpInside)
      } else {        
        self.removeTarget(self, action: #selector(performTouchUp), for: .touchUpInside)
      }

    }
  }

}

Comme vous pouvez le constater, j’ai décidé de créer un dossier dédié à touchUpInside. Je sais que les contrôles ont plus d'événements que celui-ci, mais de qui rigolons-nous? Avons-nous besoin d'actions pour chacun d'entre eux?! C'est beaucoup plus simple de cette façon.

Exemple d'utilisation: 

okBtn.touchUp = {
      print("OK")
    }

Dans tous les cas, si vous souhaitez étendre cette réponse, vous pouvez créer une Set actions pour tous les types d'événement ou ajouter d'autres propriétés d'événement pour d'autres événements. C'est relativement simple.

Salutations, M.

1
M. Porooshani

Solution similaire à celles déjà répertoriées, mais peut-être plus légère et ne repose pas sur le hasard pour générer des identifiants uniques:

class ClosureSleeve {
    let closure: ()->()

    init (_ closure: @escaping ()->()) {
        self.closure = closure
    }

    @objc func invoke () {
        closure()
    }
}

extension UIControl {
    func add (for controlEvents: UIControlEvents, _ closure: @escaping ()->()) {
        let sleeve = ClosureSleeve(closure)
        addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
        objc_setAssociatedObject(self, String(ObjectIdentifier(self).hashValue) + String(controlEvents.rawValue), sleeve,
                             objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
    }
}

Usage:

button.add(for: .touchUpInside) {
    print("Hello, Closure!")
}
1
Grant Kamin

Rapide

Après avoir essayé toutes les solutions, celle-ci a fonctionné pour moi dans tous les cas, même lorsque le bouton de la cellule de vue tableau réutilisable

import UIKit

typealias UIButtonTargetClosure = UIButton -> ()

class ClosureWrapper: NSObject {
    let closure: UIButtonTargetClosure
    init(_ closure: UIButtonTargetClosure) {
       self.closure = closure
    }
}

extension UIButton {

private struct AssociatedKeys {
    static var targetClosure = "targetClosure"
}

private var targetClosure: UIButtonTargetClosure? {
    get {
        guard let closureWrapper = objc_getAssociatedObject(self, &AssociatedKeys.targetClosure) as? ClosureWrapper else { return nil }
        return closureWrapper.closure
    }
    set(newValue) {
        guard let newValue = newValue else { return }
        objc_setAssociatedObject(self, &AssociatedKeys.targetClosure, ClosureWrapper(newValue), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
}

func addTargetClosure(closure: UIButtonTargetClosure) {
    targetClosure = closure
    addTarget(self, action: #selector(UIButton.closureAction), forControlEvents: .TouchUpInside)
}

   func closureAction() {
       guard let targetClosure = targetClosure else { return }
       targetClosure(self)
   }
}

Et ensuite vous l'appelez comme ceci:

loginButton.addTargetClosure { _ in

   // login logics

}

Ressource: https://medium.com/@jackywangdeveloper/Swift-the-right-way-to-add-target-in-uibutton-in-using-closures-877557ed9455 }

1
Musa almatri

Swift 4.2 pour UIControl et UIGestureRecognizer, et supprimer des cibles via le paradigme de propriété stockée d’extension Swift.

Classe wrapper pour le sélecteur

class Target {

    private let t: () -> ()
    init(target t: @escaping () -> ()) { self.t = t }
    @objc private func s() { t() }

    public var action: Selector {
        return #selector(s)
    }
}

Protocoles avec associatedtypes afin que nous puissions masquer le code objc_

protocol PropertyProvider {
    associatedtype PropertyType: Any

    static var property: PropertyType { get set }
}

protocol ExtensionPropertyStorable: class {
    associatedtype Property: PropertyProvider
}

Extension pour rendre la propriété par défaut et disponible

extension ExtensionPropertyStorable {

    typealias Storable = Property.PropertyType

    var property: Storable {
        get { return objc_getAssociatedObject(self, String(describing: type(of: Storable.self))) as? Storable ?? Property.property }
        set { return objc_setAssociatedObject(self, String(describing: type(of: Storable.self)), newValue, .OBJC_ASSOCIATION_RETAIN) }
    }
}

Laissez-nous appliquer la magie

extension UIControl: ExtensionPropertyStorable {

    class Property: PropertyProvider {
        static var property = [String: Target]()
    }

    func addTarget(for controlEvent: UIControl.Event = .touchUpInside, target: @escaping () ->()) {
        let key = String(describing: controlEvent)
        let target = Target(target: target)
        addTarget(target, action: target.action, for: controlEvent)
        property[key] = target
    }

    func removeTarget(for controlEvent: UIControl.Event = .touchUpInside) {
        let key = String(describing: controlEvent)
        let target = property[key]
        removeTarget(target, action: target?.action, for: controlEvent)
        property[key] = nil
    }
}

Et aux gestes

extension UIGestureRecognizer: ExtensionPropertyStorable {

    class Property: PropertyProvider {
        static var property: Target?
    }

    func addTarget(target: @escaping () -> ()) {
        let target = Target(target: target)
        addTarget(target, action: target.action)
        property = target
    }

    func removeTarget() {
        let target = property
        removeTarget(target, action: target?.action)
        property = nil
    }
}

Exemple d'utilisation:

button.addTarget {
    print("touch up inside")
}
button.addTarget { [weak self] in
    print("this will only happen once")
    self?.button.removeTarget()
}
button.addTarget(for: .touchDown) {
    print("touch down")
}
slider.addTarget(for: .valueChanged) {
    print("value changed")
}
textView.addTarget(for: .allEditingEvents) { [weak self] in
    self?.editingEvent()
}
gesture.addTarget { [weak self] in
    self?.gestureEvent()
    self?.otherGestureEvent()
    self?.gesture.removeTarget()
}
0
Eric Armstrong

Une optimisation supplémentaire (utile si vous l’utilisez dans de nombreux endroits et que vous ne voulez pas dupliquer l’appel à objc_setAssociatedObject). Cela nous permet de ne pas nous soucier d'une partie sale de objc_setAssociatedObject et de la conserver dans le constructeur de ClosureSleeve:

class ClosureSleeve {
    let closure: () -> Void

    init(
        for object: AnyObject,
        _ closure: @escaping () -> Void
        ) {

        self.closure = closure

        objc_setAssociatedObject(
            object,
            String(format: "[%d]", arc4random()),
            self,
            objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN
        )
    }

    @objc func invoke () {
        closure()
    }
}

Donc, votre extension aura l’air un peu plus propre:

extension UIControl {
    func add(
        for controlEvents: UIControlEvents,
        _ closure: @escaping ()->()
        ) {

        let sleeve = ClosureSleeve(
            for: self,
            closure
        )
        addTarget(
            sleeve,
            action: #selector(ClosureSleeve.invoke),
            for: controlEvents
        )
    }
}
0
Maciek Czarnik

Ma solution.

typealias UIAction = () -> Void;

class Button: UIButton {

    public var touchUp :UIAction? {
        didSet {
            self.setup()
        }
    }

    func setup() -> Void {
        self.addTarget(self, action: #selector(touchInside), for: .touchUpInside)
    }

    @objc private func touchInside() -> Void {
        self.touchUp!()
    }

}
0
LightinDarknessJava
class ViewController : UIViewController {
  var aButton: UIButton!

  var assignedClosure: (() -> Void)? = nil

  override func loadView() {
    let view = UIView()
    view.backgroundColor = .white

    aButton = UIButton()
    aButton.frame = CGRect(x: 95, y: 200, width: 200, height: 20)
    aButton.backgroundColor = UIColor.red

    aButton.addTarget(self, action: .buttonTapped, for: .touchUpInside)

    view.addSubview(aButton)
    self.view = view
  }

  func fizzleButtonOn(events: UIControlEvents, with: @escaping (() -> Void)) {
    assignedClosure = with
    aButton.removeTarget(self, action: .buttonTapped, for: .allEvents)
    aButton.addTarget(self, action: .buttonTapped, for: events)
  }

  @objc func buttonTapped() {
    guard let closure = assignedClosure else {
      debugPrint("original tap")
      return
    }
    closure()
  }
} 

fileprivate extension Selector {
  static let buttonTapped = #selector(ViewController.buttonTapped)
}

Ensuite, à un moment donné du cycle de vie de votre application, vous modifiez la fermeture des instances. Voici un exemple

fizzleButtonOn(events: .touchUpInside, with: { debugPrint("a new tap action") })
0
jnblanchard