リブートキャンプ by Swift 目次
まずは、UIViewの派生について。
クラスの派生自体は「こんにちは世界」で説明したとおりで、普段はUIViewとして振る舞うオブジェクトなんだけど、所々で俺流に振る舞うオブジェクトを作りたい時に使うもんです。
すでにUIViewContorollerを派生させたViewContorollerを使ってるんで、メソッドのオーバーライドもある程度把握してると思うんだけど、今回のMapViewのように作成時に独自の振る舞いをさせたい場合は、どのメソッドをオーバーライドするのか?
それがMapView.swiftでやってるinitメソッドのオーバーライドっす。
C++言語みたいに
MapView(frame:CGRect) { ・・・ }
とかがあると思ってた?
残念init(frame:)ちゃんでした。
initはイニシャライザと呼ばれている特殊なメソッドで、どのクラスでもオブジェクト作成時にはこのinitメソッドが使われる決まりになっています。
注)クラスオブジェクトのメソッドっぽいつーか、クラスオブジェクトにinitメッセージ送ってオブジェクト作ることもできるんで、多分そうなんだけど、そこんとこの詳細は不明。Appleのドキュメントには、ぱっと見「インスタンスメソッドに似ている」としか書いてないんでインスタンス側のメソッドではないってことは確か。
で、それをやってるのが前回のサンプル、MapView.swiftの
↓こいつね
サンプル:
http://tetera.jp/xcc/book-sample/modalview.zip
override init(frame: CGRect) {
super.init(frame:frame)
・・・
てとこなわけですが、イニシャライザは普通のメソッドのオーバーライドほどフリーダムじゃなくて、いくつかの制約がある点には注意が必要っす。
例えば
1)必ず派生元のイニシャライザを呼び出す必要がある
2)指定済のイニシャライザしか呼び出せない
なんてのがそうで、このうち、1)の制約は派生の初期化メソッドである以上、当たり前の話だけど、2)の方の指定済のイニシャライザって何ですかって話です。
これは、例えばMapViewクラスに引数なしのイニシャライザ作っちゃおーという風に
class MapView: UIView {
init() {
super.init()
}
なんて書くと
てな風に注意されちゃう制約です。
指定済のイニシャライザ使えって注意されてるわけでして、UIViewの場合だと
init(frame: CGRect)
が指定済のイニシャライザてことになってて、クイックヘルプでも説明(designatedてとこがそう)されてます。
ただ、正直、何が指定済のイニシャライザなのか判断する方法は微妙で…
例えばUIImageViewクラスなんかは
init(image: UIImage?)
なんてイニシャライザが追加定義されてるけど、クイックヘルプにdesignatedの単語は見当たらないし、かといってUIImageViewの派生クラスでinit(frame: CGRect)の代わりにinit(image: UIImage?)を呼んでも注意されないし…
指定済のイニシャライザは、派生元のクラス定義で
convenience … init(…
ってな風にconvenienceが付いてないイニシャライザってことで安定なのかな〜と思ったらUIViewの大元の派生クラスのNSObjectには
init()
が定義されてるのに、super.init()呼ぶと怒られるし…、どうしろと…
まあ、直接の派生元でconvenienceが付いてないイニシャライザは大丈夫だろうと…、クイックヘルプにdesignatedの単語があればなおよしって、ことでいいでしょう。
ちなみにconvenienceてのが付くイニシャライザは、内部でself.init(…て感じで、自身の別のイニシャライザを呼ぶイニシャライザです。
引数の省略なんかで用意したりする。お手軽(convenience:コンビニエンス)ツーわけですな。
で、この仕組みは派生クラスでイニシャライザ用意するときに、convenienceじゃないイニシャライザをオーバーライドすれば、convenience側イニシャライザも自動的に派生に対応できちゃう点で優れものなんですが…
このsuper.init(…じゃなくて、self.init(…なところが、まさに問題でして、このconvenience側イニシャライザを派生側で、super.init(…、派生元のイニシャライザとして呼んじゃうと、swiftの仕組みでは、派生元のイニシャライザを呼んだつもりが、回り回って派生先のイニシャライザを呼ぶことになるんですよ。
これを避けるために派生先でconvenience側のイニシャライザをsuperで呼び出すのは禁止されてるみたいっす。
ま、全部完全に防止するできてるわけじゃなく、次のように書いても注意されない点は要注意。
class LoopView : UIView {
override convenience init(frame: CGRect) {
self.init()
}
}
上の説明が理解できてる人は、このLoopViewを作っちゃうと無限ループになるってのがわかるはず。
let v = LoopView(frame:CGRect(x:0, y:0, width:100, height:100))
なんてやると、いつまでもLoopView作成から帰ってこなくなるわけだ。
それと、UIViewの派生クラスでイニシャライザを用意した場合は
init?(coder aDecoder: NSCoder)
のオーバーライドが義務付けられてます。しかもoverrideじゃなく
required init?(coder aDecoder: NSCoder) {
}
という形で書かないといけないというね。
こいつはUIViewが採用しているNSCodingプロトコルで要求されてるもので、実装が義務付けられてるイニシャライザ。
なので、UIViewの定義ではoverrideにはならないんだけど、派生先(今回ならMapView)でのオーバーライドの義務付けと、そのまた派生先でのオーバーライドの義務付けを指定するためにrequiredのキーワードが必須になるみたいっす。
本来ならUIViewクラスの定義でもrequiredキーワードを付けるべきなんじゃないかと思うんだけど付いてない。でも、init?(coder aDecoder: NSCoder)を書き忘れるとXcodeに怒られるというね、なぜだ…
あと、このinit?(coder aDecoder: NSCoder)の?はnullを戻す可能性のあるイニシャライザという意味です。この場合のイニシャライザは
return null
で戻れるようになってます。
init?(coder aDecoder: NSCoder)はNSCoderから自分に必要な設定値(今回ならframe:で受け取るCGRectの値など)を読み込んで、自分をイニシャライズするというもので、このNSCoderからの情報取り出しに失敗した時などにnullを返すようになってます。
なので、このイニシャライザを使う方はオプショナル型としての対応が必要になる。
if let v = UIView(coder:coder) {
}
ちゅー感じですね。
というわけでMapViewクラスでもinit?(coder aDecoder: NSCoder)を定義してますが、今の所MapViewクラスでNSCoderからの情報取り出す予定はないので
fatalError("init(coder:) has not been implemented")
ってして、致命的なエラーとして「init(coder:)持ってないねん」と報告だけしてアプリが死ぬようにしてます。
fatalError関数はユーザーから見るとアプリが突然死する状態なんで、使うのは開発中だけにするのが無難でしょう。今回のように絶対MapViewはNSCoderから作成されることは無い、呼び出されたら腹を切りますってくらい自信があるなら残しててもいいのかな?
私はAppleにアプリを申請するときは、機械的なメッセージ・関数呼び出しのチェックもあるかと思ってinit?(coder aDecoder: NSCoder)側も実装してfatalError関数は取っ払うようにしてるんで、残したままで審査に通るかどうかは知りません。
init?(coder aDecoder: NSCoder)に付いてはストーリーボードの話の時にでも。
ちなみにMapViewの派生先でオーバーライドする時もoverride requiredと書く必要はなく
required init?(coder aDecoder: NSCoder)
でOK。requiredは必ずoverrideでもあるんでこれでOKみたいっす。
以上でUIViewクラスの派生先でイニシャライザ実装するのは結構大変だよって話はおしまい。
ゆーても、UIViewクラスの派生先でイニシャライザ実装すると、次のようにXcodeがinit?(coder aDecoder: NSCoder) 抜けてますよって感じで、注意してもくれるし、注意文の先頭にクリックできそうな赤いマークがあれば解決策も見せてくれるようにもなってます。
そうすっと、こんなん出ます。
で、ここで出てるFixってボタンクリックすると、とりあえずの実装を用意もしてくれる。
↓ こんな感じの実装
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
困ったときは、エラーメッセージの先頭がクリック可能かチェックせよ!
てなわけで、今回はおしまい。