连接UIButton关闭? (斯威夫特,目标行动)

时间:2014-07-25 18:20:48

标签: ios swift uibutton closures

我想将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连接到目标 - 操作对的 only 方法似乎是使用仅出于向后兼容性原因而存在的内容(Selector)。

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

8 个答案:

答案 0 :(得分:23)

您可以通过添加辅助闭包装置(ClosureSleeve)并将其作为关联对象添加到控件中来替换目标操作,以便保留它。

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

Swift 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

答案 1 :(得分:19)

您认为应该在库中的一般方法,但不是:写一个类别。在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”。所以我把它包裹起来。

答案 2 :(得分:7)

这不一定是"挂钩,"但是你可以通过继承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 :(得分: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。这是炸弹。

答案 4 :(得分: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)
    }
}

答案 5 :(得分:1)

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

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

答案 6 :(得分:1)

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

你可以这样做:

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:...不保留目标,因此需要保留调度对象(与functors数组一样)。你当然不需要紧紧抓住按钮,因为你可以通过闭包中捕获的引用来做到这一点,但是你可能需要明确的引用。

PS。我试图在游乐场测试这个,但无法使sendActionsForControlEvents工作。我已经将这种方法用于手势识别器。

答案 7 :(得分:0)

关联对象,包装和指针以及ObjectiveC的导入都是不必要的,至少在Swift 3中是这样。这很有效,而且更像是Swift-y。如果你发现它更具可读性,请随意为() -> ()抛出一个typealias,我发现直接读取块签名更容易。

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