前回の記事はNotificationCenterの基本でした。
その中で、難易度的に省いたトピックを今回書きます。
偉く長いタイトルにしましたが、トピックとしては二点で、
- Selectorの話
- AddTarget()/AddObserver()に変数渡したいときどうするか
を扱います。
実行環境
Swift: 5.1.3
Xcode: 11.3.1
Selector完全攻略
NotificationCenterのAddObserver()の引数の中に、Selectorが出てきます。
このSelector、僕はUI部品にAddTarget()するときの引数で最初に遭遇して、かなり扱いに苦しみました。
let button = UIButton()
button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)
@objc func somefunction() { …… }
あるボタンをタップしたら、何か処理を動かすといったコードを書くとき、
こんな指定をすると思うんですが、正直疑問はたくさんありました。
-
@objcとは?
- これはでもなんかお約束でつけなきゃいけないし、Xcodeが補完してくれるのでそんな問題じゃない
- #Selectorってなんだ?
- Selector()でもセレクタつくれるけど、どっち使ったらいいの?
- なんか他のSwiftの構文と君全然違くない?
- クロージャの渡し方と全然違うんだけど……
- てか引数どうなってんの
- これは何? 文字型? クロージャ?
- たまにサンプルでsomefunction(_:)ってなってるの、何?
- アメリカ式の顔文字?→何わろとんねん
当時の自分の疑問を解消するような形で、説明していきたいと思います。
Selectorとは
セレクタ(Selector)は、そもそもObjective-Cの概念です。
Swiftには純粋な意味でのセレクタは存在しませんが、
それだとObjective-Cのコードとの互換性で困るので、折衷案的にSwiftでもセレクタが使えます。
Objective-Cでは、下記のように指定します。
@selector ( method )
Swiftでは実は指定する方法が三通りあります。
よく使うと思われる順で、#Selector、Selector、文字列のみの三つです。
実装方法(引数なし)
セレクタで使おうとしているメソッドに引数がないときは指定が楽なので、まずはその前提で説明します。
共通サンプルコード
説明上必要なのはAddTarget()のところだけなんですが、動作確認用に使ったサンプルコード全部を載せときます。
自分で色々試してみたい方はお使いください。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: view.center.x - 50, y: view.center.y - 50, width: 100, height: 100))
button.backgroundColor = .black
button.setTitle("Event", for: .normal)
button.titleLabel?.textColor = .white
view.addSubview(button)
//以後の説明では↓この一行だけ使います
button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)
}
@objc func somefunction() {
print("動かしたい処理を記述")
}
}
#Selector
button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)
詳しくは→Selector Expression
Selector
button.addTarget(self, action: Selector("somefunction"), for: .touchUpInside) //No method declared with Objective-C selector 'somefunction'というワーニングが出る
Swiftで#とか@とかの接頭詞がついていると、あまり利用が推奨されないものという感覚があって、
Selectorの方が#SelectorよりSwiftらしいのか? と勉強しはじめの頃は思ったのですが、
Selectorに関しては#Selectorを使う方がいいと思います。
(異論あればコメントください。#なしのSelectorの存在意義が今のところ僕の中ではイマイチ飲み込めてません……)
XcodeからWrap the selector name in parentheses to suppress this warning
という修正案が出るので、Fixを押すと、
button.addTarget(self, action: Selector(("somefunction")), for: .touchUpInside)
なぜか二重カッコに訂正されます。
まあそれはいい(よくないが)んですが、一番キツイのは、#Selectorと違って、メソッド名のコンパイラチェックがないことです。
button.addTarget(self, action: #selector(samufankusyon), for: .touchUpInside)
// →Use of unresolved identifier 'samufankusyon'
#Selectorは、メソッド名のチェックをしてくれるので、コンパイルエラーとなります。
一方で、
button.addTarget(self, action: Selector(("samufankusyon")), for: .touchUpInside)
Selectorは文字列なので存在しないメソッド名でも指定できてしまい、実行時エラーとなります。
Selectorで文字列からメソッドを呼び出す(Swift4.2)
文字列のみ
実は文字列だけ突っ込んでも、ワーニングは出ますが、なんか上手いこと解釈してくれて動きます。
button.addTarget(self, action: "somefunction", for: .touchUpInside)
//No method declared with Objective-C selector 'somefunction'
//Replace '"somefunction"' with 'Selector("somefunction")' [Fix]
動作的にはSelector("somefunction")と一緒ですね。
実装方法(引数あり)
で、引数があるときの書き方。
ちょっと冗長になりますが、3パターン全部書いてみようと思います。
(初学者の頃網羅的に書いてくれてる記事がなくて辛かった思い出があるので)
その前に
セレクタで指定したメソッドの引数に何を渡せるかを決めるのは、そのセレクタを引数にとる関数が決めます。
ここではAddTarget()です。
#selector に複数の引数を持たせたいです
↑こちらの回答にある通り、AddTarget()でとれる引数は最大2つで、
しかもイベント発生時のUIButtonとUIEventのインスタンスしか取れません。
したがって、
button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)
@objc func somefunction(some: String, number: Int) {
print("動かしたい処理を記述")
}
こんな書き方をすると、実行時エラーとなります。
ちなみに、NotificationCenterのAddObserver()の引数は、Notificationインスタンス1つです。
aSelector
Selector that specifies the message the receiver sends observer to notify it of the notification posting. The method specified by aSelector must have one and only one argument (an instance of NSNotification).
変数の型と中身は制約がありますが、変数名とセレクタの書き方は自由です。
具体的に見ていきましょう!
#Selector
button.addTarget(self, action: #selector(somefunction(sender:forEvent:)), for: .touchUpInside)
//button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)
// ↑これでも呼び出し可
// sender/forEvent/eventなどの変数名は開発者によって変更可
@objc func somefunction(sender: UIButton, forEvent event: UIEvent) {
print("動かしたい処理を記述")
print(sender.titleLabel?.text) //Optional("Event")
print(event) //<UITouchesEvent: 0x282e10640>……
}
初学者の頃よく混乱したネットに転がってるサンプルで、#selector(somefunction(_:))
と指定している例がありますが、
あれは呼び出すメソッドの外部引数名を_で省略可としているため、こんな書き方ができるんですね〜
button.addTarget(self, action: #selector(somefunction(_:_:)), for: .touchUpInside)
@objc func somefunction(_ sender: UIButton, _ event: UIEvent) {……}
コロンで変数分けてるのがSwift的には気持ち悪いですが、Objective-Cの名残りだと思われます。
Selector
Selectorの引数指定はちょっとしんどいです。
#Selectorみたいに、
button.addTarget(self, action: Selector("somefunction(sender:forEvent:)")), for: .touchUpInside)
でイケると思うじゃないですか。
実行時エラーでクラッシュします。
NSInvalidArgumentExceptionです。
Selectorで文字列からメソッドを呼び出す(Swift4.2)こちらを参考にしながら、色々試してみましたが、30分くらいクラッシュし続けました。
Selector("somefunctionWithSender:ForEvent:")
Selector("somefunction")
Selector("somefunctionWithsender:forEvent:")
Selector("somefunctionwithsender:forEvent:")
Selector("somefunctionWithSender:Event:")
Selector("somefunctionWithSender:event:")
// 頭おかしくなりそう
メソッドの引数を一つにしてみたら動いたので、もうこれ書いてお茶を濁そうかなと思ったとき、参考にしていたページを見直して、気付きました。
あ、これ第二引数、一文字目小文字やん!!!!
button.addTarget(self, action: Selector("somefunctionWithSender:forEvent:"), for: .touchUpInside)
正解はこう。
WithV1:V2:……みたいに引数を指定するのは、Objective-C方式です。
注意すべきは、第一引数はメソッド定義に関わらず一文字目を大文字に、
第二引数以降はメソッド定義に従う、というところでした。
うーんやっぱ#Selectorより無印Selectorの方がObjective-Cっぽいですよねえ。
文字列のみ
Selectorと一緒です。
NotificationCenter使うときのUserInfoの扱い方
AddTarget()を例に長々説明してきましたが、AddObserver()についても少し書かせてください。
AddObserver()はセレクタで指定したメソッドにNotification型を渡せるのですが、
NotificationにはuserInfoというプロパティがあり、postするときにちょっとしたデータを渡せます。
import Foundation
class Subject {
let eventName: Notification.Name
init(eventName: Notification.Name) {
self.eventName = eventName
}
func post() {
NotificationCenter.default.post(name: eventName, object: nil, userInfo: ["test": "Yeah"])
}
}
class Observer {
let eventName: Notification.Name
init(eventName: Notification.Name) {
self.eventName = eventName
NotificationCenter.default.addObserver(self, selector: #selector(doWhenEventOccur), name: eventName, object: nil)
// NotificationCenter.default.addObserver(self, selector: "doWhenEventOccurWithNotification:", name: eventName, object: nil)
}
@objc func doWhenEventOccur(_ notification: Notification) {
print(notification.userInfo) //Optional([AnyHashable("test"): "Yeah"])
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
let eventName = Notification.Name("Event Occurs")
let observer = Observer(eventName: eventName)
let subject = Subject(eventName: eventName)
subject.post()
※このコードについての解説は前回の記事参照
ただ[AnyHashable: Any]型という、ちょっと扱いづらい辞書型で渡るので、正直使いづらいです。
初学者特有のAddTarget()やAddObserver()のセレクタに変数を渡そうとする願望について
以上で本題は終わりなんですが、関連して。
iOSアプリ開発に手を染めて、1〜2ヶ月くらい、やたらAddTarget()のときに値を渡そうとしてもがいた記憶があります。
今思うと、なんでそんな必要があったのかな〜という気持ちなんですが、
もしかしたらこの記事をみている方も、無理やりセレクタで指定したメソッドに値渡したくてこの記事にたどり着いたかもしれないので、
そのことについて書こうと思います。
結論:セレクタで変数渡したいときはプロパティ経由で渡すよう検討する
結論的にはこれです。
Qiitaを漁ると、userInfoで渡すとか、UIButtonをExtensionで拡張するとか、やり方は色々出てきます。
ただきれいなやり方とは言えないと思います。
ケースバイケースではありますが、クラスなり構造体なりのプロパティとして渡したい変数をつくって、そこ経由で受け渡すのがベストだと思います。
なんらかの理由で渡せないなら、アクセス制御の設計がミスっているので、そこを直した方がいいです。
なぜ初学者はセレクタに変数を渡したがるのか
などと正論を書いても、実際僕も最初の頃セレクタに変数渡さないと実装できなかったシーンが何かあったような記憶がうっすらとあります。
具体的なケースは思い出せないのですが、確かにありました。
なんでだろうな〜と考えたところ、僕の中で一つの結論が出ました。
初学者がセレクタに変数を渡したい、と思うのは、ViewControllerが世界の全てだからではないでしょうか。
Xcodeで新規プロジェクトつくった際に用意されるサンプルコードを使うと、ViewControllerのViewDidLoad()から書き始めることになります。
そこでコード量が多くなって、各処理をメソッドに切り分けていきます。
ViewControllerにはデフォルトだとイニシャライザが書かれていません。
なので、ViewControllerのプロパティを作ろうとすると、イニシャライザがないよ、で怒られます。
初期値を設定してやるとか、オプショナル型にするとか、イニシャライザを真面目に書くとか、
なんらか対処できればいいのですが、いかんせん初学者なので、
「ViewControllerに直でプロパティ書くとなんか上手くいかないなあ……」で終わる(僕の場合ですが)。
となると何がなんでもセレクタに変数を渡せないと、自分がやろうとしている機能ができない! となるわけです。
遠回りのようですが、iOSアプリ全体の構造とか、クラスの使い方とかについて勉強するのが良いのだと思われます。
まとめ
というわけで、SwiftのSelectorについてでした。