【问题标题】:Hooking up UIButton to closure? (Swift, target-action)将 UIButton 连接到关闭? (迅速,目标行动)
【发布时间】:2014-09-17 16:29:38
【问题描述】:

我想将 UIButton 连接到一段代码 - 根据我的发现,在 Swift 中执行此操作的首选方法仍然是使用 addTarget(target: AnyObject?, action: Selector, forControlEvents: UIControlEvents) 函数。这使用 Selector 构造大概是为了与 Obj-C 库向后兼容。我想我理解 Obj-C 中 @selector 的原因——能够引用方法,因为在 Obj-C 中方法不是一等值。

不过,在 Swift 中,函数是一等值。有没有办法将 UIButton 连接到闭包,类似于:

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

据我所知,上述情况目前是不可能的。考虑到 Swift 看起来它的目标是变得非常实用,这是为什么呢?这两个选项显然可以共存以实现向后兼容性。为什么这不像 JS 中的 onClick() 那样工作?似乎唯一将 UIButton 连接到目标-动作对的方法是使用仅出于向后兼容性原因而存在的东西 (Selector)。

我的用例是在循环中为不同的对象创建 UIButton,然后将每个对象连接到一个闭包。 (设置标签/在字典中查找/子类化 UIButton 是肮脏的半解决方案,但我对如何在功能上做到这一点感兴趣,即这种关闭方法)

【问题讨论】:

标签: ios swift uibutton closures


【解决方案1】:

UIButton 继承自 UIControl,它处理输入的接收和转发到选择。根据文档,该操作是“识别操作消息的选择器。它不能为 NULL。” Selector 严格来说是一个指向方法的指针。

我认为鉴于 Swift 似乎将重点放在闭包上,这是可能的,但情况似乎并非如此。

【讨论】:

    【解决方案2】:

    您可以使用代理类来处理此问题,该代理类通过目标/动作(选择器)机制将事件路由到您制作的闭包。我已经为手势识别器做到了这一点,但同样的模式应该适用于控件。

    你可以这样做:

    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
    }
    

    对此的一个警告是,由于 addTarget:... 不保留目标,因此您需要保留调度对象(与仿函数数组一样)。当然,您不必严格地按住按钮,因为您可以通过闭包中捕获的引用来做到这一点,但您可能需要显式引用。

    PS。我试图在操场上对此进行测试,但无法让 sendActionsForControlEvents 工作。不过,我已经将这种方法用于手势识别器。

    【讨论】:

      【解决方案3】:

      对于您认为应该在库中但不在库中的任何内容,一般方法是:编写一个类别。 GitHub 上有很多这种特殊的,但在 Swift 中没有找到,所以我自己写了一个:

      === 把这个放到自己的文件里,比如 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)
          }
      }
      

      然后像这样调用它:

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

      显然,这可以改进,可以有各种活动的选项(不仅仅是内部修饰)等等。但这对我有用。 它比纯 ObjC 版本稍微复杂一些,因为需要一个块的包装器。 Swift 编译器不允许将块存储为“AnyObject”。所以我把它包起来了。

      【讨论】:

      • 效果很好!我确实将 block_setAction 更改为 onTouchUpInside,以反映块正在处理的事件,并能够稍后添加其他块事件。
      • 另请注意,该块由self保留,因此在块内引用self将需要“unowned self”:myButton.onTouchUpInsder { [unowned self] sender in }跨度>
      • 好点,把它放在示例代码中。我没有在动作中引用自我,所以不需要。
      • 这么好的一段代码!要为此添加更多风味,您可以将 UIButton 替换为 UIControl 并将 'UIControlEvents' 作为第二个参数传递给 block_setAction,以便与其他控件一起使用。例如文本字段的编辑已更改。
      • 如何从这里移除目标?
      【解决方案4】:

      这不一定是“挂钩”,但您可以通过继承 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")
      }
      

      【讨论】:

      • 只是一个简单的问题,你不应该在闭包弱引用中设置 self 以避免内存泄漏吗?
      【解决方案5】:

      根据n13's solution,我做了一个swift3版本。

      希望它能帮助像我这样的人。

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

      【讨论】:

        【解决方案6】:

        使用RxSwift很容易解决这个问题

        import RxSwift
        import RxCocoa
        
        ...
        
        @IBOutlet weak var button:UIButton!
        
        ...
        
        let taps = button.rx.tap.asDriver() 
        
        taps.drive(onNext: {
            // handle tap
        })
        

        编辑

        我想承认 RxSwift/RxCocoa 是一个非常重量级的依赖项,添加到项目中只是为了解决这个需求。可能有更轻量级的解决方案可用,或者只是坚持目标/行动模式。

        无论如何,如果您对处理应用程序和用户事件的通用声明式方法的想法有吸引力,那么一定要看看 RxSwift。这是炸弹。

        【讨论】:

          【解决方案7】:

          ObjectiveC 的关联对象和包装、指针和导入是不必要的,至少在 Swift 3 中是这样。这很好用,而且更加 Swift-y。如果您觉得 () -&gt; () 更具可读性,请随意在其中添加类型别名,我发现直接读取块签名更容易。

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

          【讨论】:

          • 其他方案之所以使用关联对象,是因为它是对UIButton的扩展。您的解决方案的不同之处在于它使用了子类化。这可能是一个缺点 - 如果无法使用子类。
          • 是的,关联对象的全部意义在于避免创建子类。扩展要好得多,因为人们可以在任何地方、任何按钮上简单地使用它。如果您使用 Swift,请学会爱上扩展 - 它让生活变得更轻松。
          【解决方案8】:

          您可以通过添加一个辅助闭包包装器 (ClosureSleeve) 并将其作为关联对象添加到控件中以使其保留,从而将 target-action 替换为闭包。

          这与 n13 的答案中的解决方案类似。但我发现它更简单、更优雅。更直接地调用闭包并自动保留包装器(作为关联对象添加)。

          斯威夫特 3 和 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)
              }
          }
          

          用法:

          button.addAction {
              print("Hello")
          }
          

          它会自动挂钩到.primaryActionTriggered 事件,该事件等于 UIButton 的.touchUpInside

          【讨论】:

          • 这是最适合我的。我已经对其进行了调整,并添加了删除事件侦听器以及注册一个侦听器的可能性,该侦听器在调用操作后会自行删除。在这里:gist.github.com/PEZ/e4a790870855a0bb3a45da2da8f71aa3 提及它是因为我是 Swift 新手,并不真正知道我在做什么,所以欢迎任何反馈。
          • 有一个listenOnce 听众的好主意。我不确定它是否有用,但在某些用例中它可能会派上用场。反馈已添加到 github。
          • 确保不要在闭包内捕获 self 否则会泄漏内存
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-11-13
          相关资源
          最近更新 更多