Подтвердить что ты не робот

Добавление замыкания в качестве цели в UIButton

У меня есть общий класс управления, который должен установить завершение кнопки в зависимости от контроллера вида. До этого функция setLeftButtonActionWithClosure должна принимать в качестве параметра закрытие, которое должно быть установлено как действие на кнопку. Как бы это было возможно в Swift, так как нам нужно передать имя функции как String в действие: параметр.

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

Ответ 1

ПРИМЕЧАНИЕ: как @EthanHuang сказал: "Это решение не будет работать, если у вас более двух экземпляров. Все действия будут перезаписаны последним назначением". Имейте в виду, что при разработке я скоро опубликую другое решение.

Если вы хотите добавить замыкание в качестве цели в UIButton, вы должны добавить функцию в класс UIButton, используя extension

Swift 5

import UIKit    
extension UIButton {
    private func actionHandler(action:(() -> Void)? = nil) {
        struct __ { static var action :(() -> Void)? }
        if action != nil { __.action = action }
        else { __.action?() }
    }   
    @objc private func triggerActionHandler() {
        self.actionHandler()
    }   
    func actionHandler(controlEvents control :UIControl.Event, ForAction action:@escaping () -> Void) {
        self.actionHandler(action: action)
        self.addTarget(self, action: #selector(triggerActionHandler), for: control)
    }
}

Старшая

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

и вызов:

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

Ответ 2

Решение, подобное уже перечисленным, но, возможно, более легкий вес:

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

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

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

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

Использование:

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

или

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

Или, если избегаете сохранения циклов:

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

Ответ 3

Вы можете добиться этого путем подкласса 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)
    }
}

Использование:

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

Ответ 4

Это в основном ответ от Armanoide, но с несколькими небольшими изменениями, которые мне полезны:

  • закрытое соединение может принимать аргумент UIButton, позволяя вам передать self
  • функции и аргументы переименовываются таким образом, что для меня уточняется, что происходит, например, путем выделения Swift закрытия из действия 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)
    }
    

Много реквизитов для Armanoide, хотя для какой-то тяжелой магии здесь.

Ответ 5

Я начал использовать ответ Armanoide, игнорируя тот факт, что он будет переопределен вторым назначением, главным образом потому, что сначала мне нужно было где-то конкретное, что не имело большого значения. Но он начал разваливаться.

Я придумал новую реализацию, используя AssicatedObjects, у которой нет этого ограничения, я думаю, имеет более умный синтаксис, но это не полное решение:

Вот он:

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

    }
  }

}

Как вы можете видеть, я решил сделать специальный случай для touchUpInside. Я знаю, что у контроля больше событий, чем у этого, но кого мы шутим? нужны ли нам действия для каждого из них?! Это намного проще.

Пример использования:

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

В любом случае, если вы хотите расширить этот ответ, вы можете либо сделать Set действий для всех типов событий, либо добавить дополнительные свойства событий для других событий, это относительно просто.

Cheers, М.

Ответ 6

стриж

Перепробовав все решения, это сработало для меня во всех случаях, даже когда кнопка в многократно используемой ячейке табличного представления

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

И тогда вы называете это так:

loginButton.addTargetClosure { _ in

   // login logics

}

Ресурс: https://medium.com/@jackywangdeveloper/swift-the-right-way-to-add-target-in-uibutton-in-using-closures-877557ed9455

Ответ 7

Решение, аналогичное уже перечисленным, но, возможно, более легкий вес и не использует случайность для генерации уникальных идентификаторов:

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

Использование:

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

Ответ 8

Мое решение.

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!()
    }

}

Ответ 9

Swift 4.2 для UIControl и UIGestureRecognizer, а также и удаление целей с помощью парадигмы хранимых свойств быстрого расширения.

Класс Wrapper для селектора

class Target {

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

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

Протоколы с associatedtype objc_ поэтому мы можем скрыть скрыть код objc_

protocol PropertyProvider {
    associatedtype PropertyType: Any

    static var property: PropertyType { get set }
}

protocol ExtensionPropertyStorable: class {
    associatedtype Property: PropertyProvider
}

Расширение, чтобы сделать свойство по умолчанию и доступным

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

Давайте применять магию

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

И на жесты

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

Пример использования:

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

Ответ 10

Еще одна оптимизация (полезно, если вы используете ее во многих местах и ​​не хотите дублировать вызов на objc_setAssociatedObject). Это позволяет нам не беспокоиться о грязной части objc_setAssociatedObject и хранить ее внутри конструктора 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()
    }
}

Итак, ваше расширение будет выглядеть немного чистым:

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

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

Ответ 11

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

Затем, в какой-то момент жизненного цикла вашего приложения, вы измените закрытие экземпляров. Вот пример

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

Ответ 12

Решение @Armanoide - это круто, потому что оно использует трюк с struct и static var внутри него, но оно не идеально, если вы повторно используете одну кнопку несколько раз, потому что в этом случае при закрытии действия всегда сохраняется последний обработчик.

Я исправил это для библиотеки UIKitPlus

import UIKit

extension UIControl {
    private func actionHandler(action: (() -> Void)? = nil) {
        struct Storage { static var actions: [Int: (() -> Void)] = [:] }
        if let action = action {
            Storage.actions[hashValue] = action
        } else {
            Storage.actions[hashValue]?()
        }
    }

    @objc func triggerActionHandler() {
        actionHandler()
    }

    func actionHandler(controlEvents control: UIControl.Event, forAction action: @escaping () -> Void) {
        actionHandler(action: action)
        addTarget(self, action: #selector(triggerActionHandler), for: control)
    }
}

Ответ 13

Я собрал небольшое расширение для UIControl, которое позволит вам действительно легко использовать замыкания для любых действий в любом UIControl.

Вы можете найти его здесь: https://gist.github.com/nathan-fiscaletti/8308f00ff364b72b6a6dec57c4b13d82

Вот несколько примеров этого на практике:

Настройка действия кнопки

myButton.action(.touchUpInside, { (sender: UIControl) in
    // do something
})

Обнаружение изменения значения переключателя

mySwitch.action(.valueChanged, { (sender: UIControl) in
    print("Switch State:", mySwitch.isOn)
})