UIButtonをコードの一部に接続したいと思います。これは、Swift=でaddTarget(target: AnyObject?, action: Selector, forControlEvents: UIControlEvents)
関数を使用することをお勧めする方法です。これは、おそらくObj-Cライブラリとの後方互換性のためにSelector
コンストラクトを使用します.Obj-Cで@selector
の理由を理解していると思います-Obj-Cメソッドでメソッドを参照できるからですファーストクラスの値ではありません。
In 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は非常に機能的であることを目指しているように見えますが、これはなぜですか?後方互換性のために2つのオプションが明らかに共存できるのはなぜですか。 JS?only UIButtonをターゲットとアクションのペアに接続する方法は、下位互換性の理由(Selector
)のためだけに存在するものを使用することです。
私のユースケースは、異なるオブジェクトのループにUIButtonを作成し、それぞれをクロージャーに接続することです。 (タグの設定/辞書の検索/ UIButtonのサブクラス化は汚い半解決策ですが、機能的にこれを行う方法、つまりこのクロージャーアプローチに興味があります)
Target-actionをクロージャーに置き換えるには、ヘルパークロージャーラッパー(ClosureSleeve)を追加し、関連オブジェクトとしてコントロールに追加して、コントロールを保持します。
これは、n1の答えにあるものと同様のソリューションです。しかし、私はそれがよりシンプルでエレガントだと思います。クロージャーはより直接的に呼び出され、ラッパーは自動的に保持されます(関連付けられたオブジェクトとして追加されます)。
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")
}
UIButtonの.primaryActionTriggered
と等しい.touchUpInside
イベントに自動的にフックします。
ライブラリにあるべきだと思うものに対する一般的なアプローチは、そうではありません:カテゴリを書きます。この特定のものは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」として保存することを許可していません。そのため、単にラップしました。
これは必ずしも「フック」ではありませんが、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")
}
これは RxSwift を使用して簡単に解決できます
import RxSwift
import RxCocoa
...
@IBOutlet weak var button:UIButton!
...
let taps = button.rx.tap.asDriver()
taps.drive(onNext: {
// handle tap
})
編集:
RxSwift/RxCocoaは、この1つの要件を解決するためだけにプロジェクトに追加する非常に重い依存関係であることを認識したかったのです。より軽量なソリューションが利用できる場合もあれば、ターゲット/アクションパターンに固執する場合もあります。
いずれにせよ、アプリケーションとユーザーイベントを処理するための一般的な宣言的アプローチのアイデアがあなたにアピールするなら、間違いなくRxSwiftを見てください。それは爆弾です。
n13のソリューション によると、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)
}
}
これには、ターゲット/アクション(セレクター)メカニズムを介してイベントをルーティングし、作成を終了するプロキシクラスを使用します。ジェスチャレコグナイザーに対してこれを行いましたが、コントロールについても同じパターンを保持する必要があります。
次のようなことができます:
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
}
これの1つの注意点は、addTarget:...がターゲットを保持しないため、ディスパッチオブジェクトを保持する必要があることです(functors配列で行われるように)。もちろん、厳密にボタンを保持する必要はありません。クロージャ内のキャプチャされた参照を介して行うことができますが、おそらく明示的な参照が必要になるでしょう。
PS。プレイグラウンドでこれをテストしようとしましたが、sendActionsForControlEventsを機能させることができませんでした。しかし、私はこのアプローチをジェスチャレコグナイザーに使用しました。
UIButtonは、入力の受信と選択への転送を処理するUIControlを継承します。ドキュメントによると、アクションは「アクションメッセージを識別するセレクタです。NULLにすることはできません。」また、セレクターは厳密にはメソッドへのポインターです。
強調SwiftはClosuresに置かれているように思えますが、これは可能でしょうが、そうではないようです。
少なくともSwift 3.では、ObjectiveC、ObjectiveCのラッピングとポインター、およびインポートは不要です。これは非常に効果的で、Swift-yになります。_() -> ()
_より読みやすい場合は、ブロック署名を直接読む方が簡単だと思います。
_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?()
}
}
_