58

我有一个通用控件类,它需要根据视图控制器设置按钮的完成。由于 setLeftButtonActionWithClosure 函数需要将闭包作为参数,该闭包应该设置为取消按钮的操作。在 Swift 中怎么可能因为我们需要将函数名作为字符串传递给 action: 参数。

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

19 回答 19

144

在 iOS 14 中,Apple 终于将这个功能添加到了 UIKit 中。但是,由于 Apple 的方法签名不是最理想的,因此有人可能仍想使用此扩展。

iOS 14

extension UIControl {
    func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: @escaping()->()) {
        addAction(UIAction { (action: UIAction) in closure() }, for: controlEvents)
    }
}

iOS 14 之前的版本

extension UIControl {
    func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: @escaping()->()) {
        @objc class ClosureSleeve: NSObject {
            let closure:()->()
            init(_ closure: @escaping()->()) { self.closure = closure }
            @objc func invoke() { closure() }
        }
        let sleeve = ClosureSleeve(closure)
        addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
        objc_setAssociatedObject(self, "\(UUID())", 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()
}

(此处包含扩展:https ://github.com/aepryus/Acheron )

另请注意,理论上 .primaryActionTriggered 可以取代 .touchUpInside,但它目前似乎在催化剂中存在问题,所以我暂时保留它。

于 2017-01-03T07:53:54.147 回答
37

不要使用此答案,请参阅下面的注释

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

如果要将闭包作为目标添加到 a UIButton,则必须UIButton使用extension

斯威夫特 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: .touchUpInside, 
 ForAction:{() -> Void in
     print("Touch")
 })
于 2015-12-01T09:29:07.947 回答
22

您可以通过继承 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")
}
于 2016-06-28T21:14:33.910 回答
8

与已经列出的解决方案类似,但重量可能更轻,并且不依赖随机性来生成唯一 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!")
}

或者如果避免保留循环:

button.add(for: .touchUpInside) { [unowned self] in
    self.doStuff()
}
于 2018-03-13T14:44:38.753 回答
7

这现在可以在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!")
})
于 2020-07-10T16:25:04.030 回答
3

这基本上是上面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 有很多道具。

于 2016-04-15T15:24:48.093 回答
2

struct@Armanoide 解决方案很酷,因为它在其中和内部使用技巧,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)
    }
}
于 2019-10-07T00:42:17.253 回答
2

这是aepryus答案的一个有趣的变体。我的版本使用 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否则处理程序将立即取消注册。

于 2020-07-27T15:11:39.297 回答
2

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

我使用objc_setAssociatedObjectandobjc_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
})
于 2021-04-23T18:28:52.150 回答
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 {}
于 2020-05-22T11:35:05.783 回答
2

我为 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)
})
于 2019-10-16T17:49:28.663 回答
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

于 2018-02-26T15:49:49.983 回答
1

我的解决方案。

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

}
于 2018-09-17T17:57:55.873 回答
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)
    }
}

带有 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()
}
于 2018-10-13T19:26:22.597 回答
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)")
}
于 2020-02-13T15:46:23.903 回答
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") })
于 2018-02-26T16:48:09.970 回答
0

我已经开始使用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为所有事件类型进行操作,或者为其他事件添加更多事件属性,这相对简单。

干杯,M。

于 2016-11-10T06:49:20.370 回答
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
        )
    }
}
于 2017-05-18T16:47:13.170 回答
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")
}           
于 2021-04-21T04:42:59.443 回答