将闭包作为目标添加到UIButton

时间:2014-09-18 18:04:51

标签: ios swift uibutton addtarget

我有一个通用的控件类,它需要根据视图控制器设置按钮的完成。由于setLeftButtonActionWithClosure函数需要将一个闭包作为参数,该闭包应设置为解开的动作。如何可能在Swift中,因为我们需要将函数名称作为String传递给action:parameter。

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

19 个答案:

答案 0 :(得分:80)

与已经列出的类似的解决方案,但可能更轻的重量:

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, "[\(arc4random())]", sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
    }
}

用法:

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

或者如果避免保留循环:

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

用户MH175提到如果他们使用控件上的“allTargets属性:静态Set_unnconditionalBridgeFromObjectiveC(_ :) - ”,将会获得运行时异常。从NSObject扩展ClosureSleeve将解决问题:

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

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

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

答案 1 :(得分:25)

注意: 喜欢@EthanHuang说 “如果你有两个以上的实例,这个解决方案就不起作用了。所有的操作都会被最后一个任务覆盖。” 开发时请记住这一点,我会尽快发布另一种解决方案。

如果要将关闭作为目标添加到validate,则必须使用UIButton

UIButton类添加一个函数

Swift 5

extension

<强>旧版

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

答案 2 :(得分:13)

您可以通过继承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")
}

答案 3 :(得分:3)

这基本上是Armanoide's回答,但是有一些对我有用的微小变化:

  • 传入的闭包可以采用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)
    }
    

虽然在这里有一些重型魔法,但很多道具给了阿玛诺德。

答案 4 :(得分:2)

这是一种通用的Swift 5方法。它在动作块内有一个发件人,并且消除了两次为同一事件添加动作

import UIKit

protocol Actionable {
    associatedtype T = Self
    func addAction(for controlEvent: UIControl.Event, action: ((T) -> Void)?)
}

private class ClosureSleeve<T> {
    let closure: ((T) -> Void)?
    let sender: T

    init (sender: T, _ closure: ((T) -> Void)?) {
        self.closure = closure
        self.sender = sender
    }

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

extension Actionable where Self: UIControl {
    func addAction(for controlEvent: UIControl.Event, action: ((Self) -> Void)?) {
        let previousSleeve = objc_getAssociatedObject(self, String(controlEvent.rawValue))
        objc_removeAssociatedObjects(previousSleeve as Any)
        removeTarget(previousSleeve, action: nil, for: controlEvent)

        let sleeve = ClosureSleeve(sender: self, action)
        addTarget(sleeve, action: #selector(ClosureSleeve<Self>.invoke), for: controlEvent)
        objc_setAssociatedObject(self, String(controlEvent.rawValue), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
    }
}

extension UIControl: Actionable {}

答案 5 :(得分:1)

我已经开始使用 Armanoide 的答案,而不考虑它会被第二项任务覆盖的事实,主要是因为起初我需要它特定的地方,它没有& #39;重要。但它开始分崩离析。

我已经想出了一个使用 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个操作,或为其他事件添加更多事件的属性,它是相对简单。

干杯, 微米。

答案 6 :(得分:1)

<强>夫特

在尝试了所有解决方案之后,即使在可重复使用的表格视图单元格中的按钮

,这个也适用于所有情况
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 :(得分:1)

与已经列出的类似的解决方案,但可能更轻,并且不依赖于随机性来生成唯一ID:

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 :(得分:1)

这里是answeraepryus的有趣变体。我的版本使用Combine的Cancellable协议:

  1. 支持删除已注册的封包。
  2. 处理内存管理,从而避免了使用objc_setAssociatedObject的需要。
// Swift 5

import Combine
import UIKit

class BlockObject: NSObject {
    let block: () -> Void

    init(block: @escaping () -> Void) {
        self.block = block
    }

    @objc dynamic func execute() {
        block()
    }
}

extension UIControl {
    func addHandler(
        for controlEvents: UIControl.Event,
        block: @escaping () -> Void)
        -> Cancellable
    {
        let blockObject = BlockObject(block: block)
        addTarget(blockObject, action: #selector(BlockObject.execute), for: controlEvents)

        return AnyCancellable {
            self.removeTarget(blockObject, action: #selector(BlockObject.execute), for: controlEvents)
        }
    }
}

用法:

let button = UIButton(type: .system)

// Add the handler
let cancellable = button.addHandler(for: .touchUpInside) {
    print("Button pressed!")
}

// Remove the handler
cancellable.cancel()

别忘了存储对Cancellable的引用,否则处理程序将立即注销。

答案 9 :(得分:1)

Swift 4.2(用于UIControl和UIGestureRecognizer),并通过swift扩展存储的属性范例删除目标。

选择器的包装器类

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_代码

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 :(得分:1)

现在可以在iOS 14上进行此操作。创建UIAction时,您可以传递带有处理程序闭包的UIButton

let action = UIAction(title: "") { action in
    print("Button tapped!")
}

UIButton(type: .system, primaryAction: action)

或更短:

UIButton(type: .system, primaryAction: UIAction(title: "") { action in
    print("Button tapped!")
})

答案 11 :(得分:1)

我对UIControl进行了扩展,可以让您真正轻松地对任何UIControl上的任何操作使用闭包。

您可以在这里找到它:https://gist.github.com/nathan-fiscaletti/8308f00ff364b72b6a6dec57c4b13d82

以下是一些实际的例子:

设置按钮操作

<xsl:template match="/">
    <xsl:for-each-group select="//Measurement" group-by="../@name">
        <xsl:element name="Level">
            <xsl:attribute name="name" select="current-grouping-key()"/>
            <xsl:for-each select="current-group()">
                <xsl:copy-of select="current()"/>
            </xsl:for-each>
        </xsl:element>
    </xsl:for-each-group>
</xsl:template>

检测更改值的开关

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

答案 12 :(得分:1)

这是一个很好的框架:HandlersKit。最大的优点是,您可以在闭包内部访问发件人,而无需进行类型转换或可选的拆包。

UIButton示例:

import HandlersKit

let button = MyActivityIndicatorButton()
button.onTap { (sender: MyActivityIndicatorButton) in
    sender.showActivityIndicator()
}

UISwitch的示例:

let switchView = UISwitch(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 50.0))
switchView.onChange { isOn in
    print("SwitchView is: \(isOn)")
}

答案 13 :(得分:0)

我更改了@Nathan F 发布的 UIControl 的一个小扩展。 here

我使用 objc_setAssociatedObjectobjc_getAssociatedObject 来获取/设置闭包,并使用所有创建的按钮的键删除了全局静态变量。 所以现在为每个实例存储事件并在 dealloc 后释放

extension UIControl {
    
    typealias Handlers = [UInt:((UIControl) -> Void)]
    
    private enum AssociatedKey {
        static var actionHandlers = "UIControl.actionHandlers"
    }

    /**
     * A map of closures, mapped as  [ event : action ] .
     */

    private var actionHandlers: Handlers {
        get {
            return objc_getAssociatedObject(self, &AssociatedKey.actionHandlers) as? Handlers ?? [:]
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKey.actionHandlers, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        }
    }
}

您可以在这里找到它:https://gist.github.com/desyatov/6ed83de58ca1146d85fedab461a69b12

以下是一些示例:

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

答案 14 :(得分:0)

下面的扩展是为 UIView 的关卡添加点击手势,它适用于任何基于 UIView 的东西。

注意:我几年前在 StackOverflow 上也找到了这个解决方案,但现在我似乎找不到原始来源。

extension UIView {
    
    // In order to create computed properties for extensions, we need a key to
    // store and access the stored property
    fileprivate struct AssociatedObjectKeys {
        static var tapGestureRecognizer = "MediaViewerAssociatedObjectKey_mediaViewer"
    }
    
    fileprivate typealias Action = (() -> Void)?
    
    // Set our computed property type to a closure
    fileprivate var tapGestureRecognizerAction: Action? {
        set {
            if let newValue = newValue {
                // Computed properties get stored as associated objects
                objc_setAssociatedObject(self, &AssociatedObjectKeys.tapGestureRecognizer, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
            }
        }
        get {
            let tapGestureRecognizerActionInstance = objc_getAssociatedObject(self, &AssociatedObjectKeys.tapGestureRecognizer) as? Action
            return tapGestureRecognizerActionInstance
        }
    }
    
    // This is the meat of the sauce, here we create the tap gesture recognizer and
    // store the closure the user passed to us in the associated object we declared above
    public func addTapGestureRecognizer(action: (() -> Void)?) {
        self.isUserInteractionEnabled = true
        self.tapGestureRecognizerAction = action
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
        tapGestureRecognizer.cancelsTouchesInView = false
        self.addGestureRecognizer(tapGestureRecognizer)
    }
    
    // Every time the user taps on the UIImageView, this function gets called,
    // which triggers the closure we stored
    @objc fileprivate func handleTapGesture(sender: UITapGestureRecognizer) {
        if let action = self.tapGestureRecognizerAction {
            action?()
        } else {
            print("no action")
        }
    }
    
}

用法示例:

let button = UIButton()
button.addTapGestureRecognizer {
    print("tapped")
}
        
let label = UILabel()
label.addTapGestureRecognizer {
    print("label tapped")
}           

答案 15 :(得分:0)

@Armanoide解决方案很酷,因为它在内部使用了structstatic 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)
    }
}

答案 16 :(得分:0)

我的解决方案。

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

}

答案 17 :(得分:0)

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

答案 18 :(得分:0)

另一项优化(如果您在很多地方使用它并且不想复制对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
        )
    }
}