【Swift UIKit】#selectorとは?使い方と@objcとsender、dynamicの意味まとめ

この記事からわかること

  • SwiftUIKit
  • #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の機能

参考文献:Objective-C:Selector

#selectorで実装している機能はSwiftのものではなく、前身であるObjective-Cの機能のようです。

Objective-Cではセレクタとはメソッドの内部表現のことで、開発者からすると「メソッド=セレクタ」のような認識になると思います。ですが正確にはメソッドを特定する役割をもった概念であり、メソッドそのものではないのだと思います。

実際に使用する際も引数にメソッド名を渡す必要があり、引数に渡されたメソッドを検索し特定します。以下の例ではViewControllerクラスのbuttonTappedメソッドを指定しています。

button.addTarget(self,
    action: #selector(ViewController.buttonTapped),
    for: .touchUpInside)

セレクタのメリット

addTargetメソッドを例にすると_ targetにはメソッドを定義しているクラスを、actionには実際に処理する内容を、forには処理実行させるタイミング(イベント)を定義する必要があります。

これでイベントが発生した時に内部的に呼び出すメソッドを指定しています。このようにセレクタを使用することで実行時に呼び出すべきメソッドを状況に応じて切り替えるようなプログラムを実現できるようです。

Selector構造体

公式リファレンス: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に置き換えてね)といった警告が発生します。

警告:Use '#selector' instead of explicitly constructing a '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}
// )}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index