#selector、@objc とターゲット-アクション

iOS アプリのコードを書いていると、時々 #selector とか @objc などを書くときがあります。 Swift の基本的なシンタックスには出てこないものですが、一体これらは何でしょうか?

ここでは、#selector と @objc をなぜ使うのか?いつ使うのか?どう使えばいいのか?、ということを少し突っ込んでみてみましょう。

まずはボタンのイベント処理の例から

通常はボタンを画面に配置して、そのイベント処理を行う場合は、インターフェイスビルダーでボタンを置き、アシスタントエディタを開いて、アクションを設定する、という風に行いますよね。

しかしここでは、インターフェイスビルダーを使わないで、コードだけでボタンを一つ配置します。そして、そのボタンをタップしたときに print でデバッグトレースを出力してみましょう。

単純な Single View App プロジェクトを作成して、ビューコントローラを次のように書き換えます。

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let lightGray = UIColor.init(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0)

        let button = UIButton(type: .roundedRect)
        button.backgroundColor = lightGray
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        button.setTitle("Button1", for: .normal)
        button.frame = CGRect(x: 100, y: 100, width: 100, height: 40)

        view.addSubview(button)
    }

    @objc func buttonTapped(){
        print("buttonTapped called")
    }

}

これを Xcode で実行すると、確かに buttonTapped メソッドが呼ばれ、buttonTapped called という文字が出力されるのがわかると思います。

これで、ちゃんとボタンのイベント処理が実行されていることが確認できました。

さて、ここでボタンのイベント処理メソッドの指定箇所をよくみてみましょう。次の部分です。

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

  @objc func buttonTapped(){
    print("buttonTapped called")
  }

ここで button という変数には、 UIButton のオブジェクトがセットされています。 それの addTarget メソッドを呼び出すことで、buttonTapped というメソッドをイベント処理メソッドとして登録しています。

ここで処理するイベントの種類は .touchUpInside を指定しています。

さらによく見ると、buttonTapped メソッドの定義では、先頭に @objc という修飾子もついています。

今回のポイントは、このときに「なんで、メソッドを指定するのにやれ #selector だ、やれ @objc だ、とかつけないといけないの?」という点です。

この意味を順番にみていきましょう。

target-action は Objective-C で実装された仕組み

iOS アプリケーション・フレームワークの中で、アプリケーションの実行に直接関わるフレームワーク (クラスライブラリ) は Cocoa Touch といいます。

もともとは macOS (OS X) に Cocoa フレームワークがあり、それを iOS 向けにしたのが Cocoa Touch です。

本来 Cocoa の発音は「コゥコ」とか「ココ」なので、Cocoa Touch は「ココタッチ」ですが、日本語の資料では「ココア」とか「ココアタッチ」と 書き下されることが多いようです。日本語的にココと言ったら通じないので、無理にローマ字読みしなくてもいいと思います。 ・・・といっても、日本で人前でココタッチと言って笑われても責任とらないですけどね

Cocoa Touch の中でも特に UIKit というフレームワークがユーザーインターフェイスを受け持ち、UIKit ではウィンドウやイベント処理などの仕組みなどが実装されています。

Cocoa Touch フレームワーク内で定義されるクラスのインスタンスは、Cocoa Touch オブジェクトと呼ばれたりします。

さて、Cocoa Touch ではイベント処理については Objective-C の API であるターゲット・アクション (target-action) を利用しています。

target-action では、Window イベントが発生した時に、どのオブジェクトのどのメソッドを呼び出すかを指定します。 Objective-Cではメソッドを選択するパラメータをセレクタといいます。

もう一度コードをみます。イベント処理メソッドの設定箇所はここです。

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

target-action で指定するのは Objective-C のメソッドである必要があります。Swift では #selector によって、Objective-C のメソッドを指定できるので、それを使っています。

つまりこの箇所は、buttontouchUpInside イベントが発生した時には、 self オブジェクトの、#selector で指定した buttonTapped メソッドを呼ぶように指定した、ということになります。

そして、buttonTapped メソッド本体は、Objective-C のメソッドとして認識されるように @objc 属性を指定したというわけです。

#selector で指定できるメソッドの種類と指定方法

上で、#selector ではイベント処理メソッドを指定する、ということを説明しました。#selector ではどんなメソッドを指定すれば良いのでしょうか?

UIKitでは次の 3 種類のタイプのメソッドを、イベント処理メソッドとしてサポートします。

  • @objc func メソッド名()
  • @objc func メソッド名(sender: Any?)
  • @objc func メソッド名(sender: Any?, forEvent event: UIEvent?)

UI 要素の基本クラスである UIControl クラスで、上記の3種類のメソッドをサポートするようになっています。 UIButton も UIControl のサブクラスですから、同様の仕組みを受け継ぎます。

このように、同名でシグネチャ違いのメソッドがある場合は、単に #selector(buttonTapped) と指定しても、 区別できず、曖昧である (ambiguous) 旨のエラーが発生しビルドできません。

その場合は #selector でシグネチャを付けて指定します。例えば、二つの引数を受け取るメソッドを指定するなら #selector(buttonTapped(_:forEvent:)) とします。

以下に、実際に上記 3 パターンのメソッドを記述し、渡された引数の内容をプリントする例を示します。

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let lightGray = UIColor.init(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0)

        let button = UIButton(type: .roundedRect)
        button.backgroundColor = lightGray
        button.addTarget(
          self,
          action: #selector(buttonTapped(_:forEvent:)),
          for: .touchUpInside)
        button.setTitle("Button1", for: .normal)
        button.frame = CGRect(x: 100, y: 100, width: 100, height: 40)

        view.addSubview(button)
    }

    @objc func buttonTapped(){
        print("buttonTapped called")
    }

    @objc func buttonTapped(_ sender: UIButton){
        print("buttonTapped(_:) called")
        printType(sender)
    }

    @objc func buttonTapped(_ sender: UIButton, forEvent event: UIEvent){
        print("buttonTapped(_:forEvent:) called")
        printType(sender)
        printType(event)
    }

    func printType(_ obj: Any?) {
        var s = ""
        if let o = obj {
            s = String(describing: o)
        } else {
            s = "null"
        }
        print("- type = \(s)")
    }
}

上の例ではセレクタを #selector(_:forEvent:) としているので、二つパラメータを受け取るバージョンのメソッドを指定していることになります。

なお、パラメータなしの場合は、少し工夫が必要です。 この場合は明示的にキャストし、#selector(buttonTapped as ()->(Void)) とします。

以上で、Swift で target-action を利用する時のメソッドの種類と #selector の使い方を説明しました。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Swift による iOS 開発入門