web-dev-qa-db-ja.com

UIButtonをクロージャーに接続しますか? (スウィフト、ターゲットアクション)

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のサブクラス化は汚い半解決策ですが、機能的にこれを行う方法、つまりこのクロージャーアプローチに興味があります)

38
rafalio

Target-actionをクロージャーに置き換えるには、ヘルパークロージャーラッパー(ClosureSleeve)を追加し、関連オブジェクトとしてコントロールに追加して、コントロールを保持します。

これは、n1の答えにあるものと同様のソリューションです。しかし、私はそれがよりシンプルでエレガントだと思います。クロージャーはより直接的に呼び出され、ラッパーは自動的に保持されます(関連付けられたオブジェクトとして追加されます)。

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

UIButtonの.primaryActionTriggeredと等しい.touchUpInsideイベントに自動的にフックします。

25
Marián Černý

ライブラリにあるべきだと思うものに対する一般的なアプローチは、そうではありません:カテゴリを書きます。この特定のものは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」として保存することを許可していません。そのため、単にラップしました。

19
n13

これは必ずしも「フック」ではありませんが、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")
}
7
Jacob Caraballo

これは 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を見てください。それは爆弾です。

6
David James

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)
    }
}
5
Steven Jiang

これには、ターゲット/アクション(セレクター)メカニズムを介してイベントをルーティングし、作成を終了するプロキシクラスを使用します。ジェスチャレコグナイザーに対してこれを行いましたが、コントロールについても同じパターンを保持する必要があります。

次のようなことができます:

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を機能させることができませんでした。しかし、私はこのアプローチをジェスチャレコグナイザーに使用しました。

1
Chris Conover

UIButtonは、入力の受信と選択への転送を処理するUIControlを継承します。ドキュメントによると、アクションは「アクションメッセージを識別するセレクタです。NULLにすることはできません。」また、セレクターは厳密にはメソッドへのポインターです。

強調SwiftはClosuresに置かれているように思えますが、これは可能でしょうが、そうではないようです。

1
Jonah Katz

少なくとも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?()
    }
} 
_
0
Nate Birkholz