【Swift UIKit】#selectorとは?使い方と@objcとsender、dynamicの意味まとめ
この記事からわかること
- SwiftのUIKit
- #selectorや@objc/dynamic修飾子とは?
- Objective-CのセレクタをSwiftで使用する
- Selector構造体の
使い方
と注意点 - 引数に渡す
sender
などの記述方法
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
SwiftのUIKitでaddTargetメソッドの引数などに出てくるSelector
がどのようなものなのか自分なりにまとめました。
【Swift UIKit】addTargetメソッドの使い方と意味!UIControlとEventの種類
Swiftで登場する#selector
SwiftのUIKitフレームワークでControl系のビューを構築する際にビューに対してアクション(メソッド)を紐づける役割として以下のように#selector
なるものを記述します。
ボタンタップで文字を出力するサンプル
class ViewController: UIViewController {
let button = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
let screenWidth = self.view.frame.width
let screenHeight = self.view.frame.height
button.frame = CGRect(x:screenWidth/4, y:screenHeight/2,
width:screenWidth/2, height:60)
button.setTitle("ボタン", for:UIControl.State.normal)
button.addTarget(self,
action: #selector(ViewController.buttonTapped),
for: .touchUpInside)
self.view.addSubview(button)
}
@objc func buttonTapped()) {
print("ボタンをタップされたよ")
}
}
こいつの正体を突き止めるために公式リファレンスを模索してみました。すると以下のような記事がありました。
参考文献:Using Objective-C Runtime Features in Swift
Objective-Cの機能
#selector
で実装している機能はSwiftのものではなく、前身であるObjective-Cの機能のようです。
Objective-Cではセレクタとはメソッドの内部表現のことで、開発者からすると「メソッド=セレクタ」のような認識になると思います。ですが正確にはメソッドを特定する役割をもった概念であり、メソッドそのものではないのだと思います。
実際に使用する際も引数にメソッド名を渡す必要があり、引数に渡されたメソッドを検索し特定します。以下の例ではViewControllerクラスのbuttonTappedメソッドを指定しています。
button.addTarget(self,
action: #selector(ViewController.buttonTapped),
for: .touchUpInside)
セレクタのメリット
addTarget
メソッドを例にすると_ target
にはメソッドを定義しているクラスを、action
には実際に処理する内容を、for
には処理実行させるタイミング(イベント)を定義する必要があります。
これでイベントが発生した時に内部的に呼び出すメソッドを指定しています。このようにセレクタを使用することで実行時に呼び出すべきメソッドを状況に応じて切り替えるようなプログラムを実現できるようです。
Selector構造体
@frozen public struct Selector : ExpressibleByStringLiteral
Swiftではセレクタを使用できるようにするためにSelector
構造体が定義されています。一番よく見るのが#selector
式だと思いますが、Selector
構造体のイニシャライザを使用しても正常に動作します。
まずは1つ1つみていきます。
#selector式
Swiftでセレクタを使用するためにSwift専用の#selector式なるものが用意されています。Objective-Cでは@selector
コンパイラディレクティブを使用するのでObjective-Cのコードを記述しているわけではないようです。
使い方
#selector式は引数に対象となるメソッドを渡します。addTargetメソッドの場合はメソッドを定義しているクラスを明示的に指定するのでViewController.
部分を省略しメソッド名だけを記述しても問題はないですが、可読性のことを考えると記述しておいた方が良いと思います。
#selector(ViewController.buttonTapped)
#selector(buttonTapped)
イニシャライザを使用する
SwiftではSelector
構造体のイニシャライザを使用することでもセレクタ機能を実装することができます。イニシャライザの場合は引数に文字列形式でメソッド名を指定します。
button.addTarget(self,
action: Selector("buttonTapped"),
for: .touchUpInside)
ですが以下画像のようにUse '#selector' instead of explicitly constructing a 'Selector'(#selectorに置き換えてね)といった警告が発生します。
また#selector
式を使用した場合は上記のようにコード補完機能が働き、コーディングをアシストしてくれますが、イニシャライザの場合はコード補完は働きません。
注意点:コンパイルエラーを引き起こす可能性
さらに引数にはコード補完なしで文字列形式でメソッド名を渡す必要があるため、メソッド名を1文字間違えたりするだけで動作しなくなってしまいます。
button.addTarget(self,
action: Selector("buttonTpped"),
// Tapped →tppedに誤字
for: .touchUpInside)
さらにタチが悪いのがそのままビルドができてしまう点です。コード補完がないため誤字が起きていることをコンパイラが認識せずに動作させ、実行された時に初めて以下のようなエラーを吐きアプリが停止してしまいます。
"-[UIKitTest.ViewController buttonTpped]: unrecognized selector sent to instance 0x15cd0a240"
イニシャライザを使用するメリットはいったい何なのでしょうか...?
呼び出すメソッドには@objcを付与する
セレクタで指定するメソッドには@objc
を付与する必要があります。Objective-Cの機能であるセレクタを使用するには渡すメソッドももちろんObjective-Cでないといけません。SwiftのメソッドとObjective-Cのメソッドには互換性がなく、そのまま使用はできないので@objc
を付与することでObjective-Cからも利用できるようにしています。
@objc func buttonTapped() {
print("ボタンをタップされたよ")
}
以下の記事に詳細なことが書かれていたのでリンクを掲載しておきます。
参考記事:[Swift] @objcの話 [Objective-C]
@objcとdynamic修飾子
Objective-Cの機能を使用する際に必要な修飾子である@objc
ですが、似たようなものにdynamic
もあります。
調べてみるとなかなか難しそうだったので参考記事を載せておきます。
参考記事:Hipster Swift(Swift通) - Swift の秘密を解き明かす
セレクタで渡せる引数
公式リファレンス:UIControl-Respond to user interaction
#selector
で指定するメソッドには以下のように引数を持たせることができます。渡せるのは呼び出しているUIControlインスタンス
とUIEvent
です。
#selector(ViewController.buttonTapped)
#selector(ViewController.buttonTapped(sender:)),
#selector(ViewController.buttonTapped(sender:forEvent:)),
@IBAction func buttonTapped()
@IBAction func buttonTapped(sender: UIButton)
@IBAction func buttonTapped(sender: UIButton, forEvent event: UIEvent)
その場合はセレクタ側にsender:forEvent:
のようにカンマで区切らず指定し、定義しているメソッド側にも通常のメソッドの引数のように指定する必要があります。
button.addTarget(nil,
action: #selector(ViewController.buttonTapped(sender:forEvent:)),
for: .touchUpInside)
// 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
@objc func buttonTapped(sender: UIButton, forEvent event: UIEvent){
print(sender.titleLabel?.text)
print(event)
}
// Optional("ボタン")
// <UITouchesEvent: 0x600003714000> timestamp: 774444 touches: {(
// <UITouch: 0x139b041a0> phase: Ended tap count: 1 force: 0.000 window: <UIWindow: 0x136807120; frame = (0 0; 428 926); gestureRecognizers = <NSArray: 0x600000c5a9a0>; layer = <UIWindowLayer: 0x600000c5a7c0>> view: <UIButton: 0x1368050b0; frame = (107 463; 214 60); opaque = NO; layer = <CALayer: 0x60000022be40>> location in window: {202, 487} previous location in window: {202, 487} location in view: {95, 24} previous location in view: {95, 24}
// )}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。