リブートキャンプ 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")

    }

 
 
 困ったときは、エラーメッセージの先頭がクリック可能かチェックせよ!
てなわけで、今回はおしまい。
 
 
AD

リブートキャンプ by Swift 目次

リブートキャンプ by Swift 目次

 

1、ラノベっぽい

 

2、ラノベっぽくない

 

3、走れマイアプリちゃん

 

4、IBMのSwiftサイト

 

5、ど〜こ〜で〜もプログラミングぅ

 

6、ループの話

 

7、ifの話

 if文、Int型からDouble型への変換

 

8、そしてiPhoneアプリへ

 

9、「こんにちは世界」

 

10、こんにちはデベロッパの世界

 

11、スクールカーストとかヒエラルキとか

 

12、または私は如何にして心配するのを止めて…

 

13、色々と脱線

 クラスオブジェクト

 

14、重箱の隅をつつくようにネチネチと進めてみる

 プロトコル、プロトコルの採用

 

15、CALayerで完璧

 

16、ダイナミックな文字

 dynamic type対応

 

17、アンラップしてチン♪

 オプショナル型、アンラップ、アンラップ済み、

 

18、ダイナミックなレイアウト

 auto layout

 

19、タップしてドン

 

20、スクリプトエンジンっぽい

 

21、キャッチして

 

22、プレゼントせずにモーダル

 

23、UIViewの派生でイニシャライザって…

 

 init、指定イニシャライザ、convenience、required

AD

リブートキャンプ by Swift 目次

 

 場所移動用のマップ画面なんかは、今までに得た知識で十分表示できるわけでして…

 要するに、今のself.viewに新しいUIView追加すりゃいいわけですよ。

 でもって、用がなくなれば画面を隠すなり、取り外すなりすればいいわけです。

 ちなみにUIViewの取り外しは、addSubviewの時のように親側UIViewじゃなく、取り外される子側のUIViewにremoveFromSuperview()メッセージを送ります。

 

取り付ける時:

  親UIViewオブジェクト.addSubview( 子になるUIViewオブジェクト )

 

取り外す時:

  子UIViewオブジェクト.removeFromSuperview()

 

 で、追加するUIViewの中に、サブ画面としてUIImageViewとか貼り付けて地図出すとか、そのまた上に別のUIImageView貼り付けて、キャラクタのアイコンなりを表示させて現在位置を示すとか、色々やればいいわけです。

 なんならアニメーション付きでマップ画面出しましょうか、くらいのこと言う猛者もいるとは思うし、それで正解ちゃ正解なんですが…

 

 そうやって調子こいて、今までみたいにViewControllerに処理追加していくと、どんどんViewControllerのソースコードが肥大化するわけですな。

 で、半年くらい時間あけて見直した時に、コードが込み入りすぎてて「何やりたかったんだ、俺?」みたいなことになっちゃう。

 今でも十分、リファクタイリング(再因子化。現行のソースコードの関数やクラス定義を見直して、見通しの良い構造に組み替える)したいレベルなんですが、これ以上悪化させないためにはどうするか?

 こういう場合、UIViewを派生させてマップ画面専用のUIView派生クラスを作ったりします。

 

 class MapView : UIView {

   ・・・

  }

 

 そのままViewController.swiftにMapViewクラスの定義を書いてもいいんだけど、クラス定義ってのは、ファイルを分離するのに、ちょうどいい感じの単位なんで、大概は別ファイルにします。

 新しいswiftファイルを作るには、File→New→File…メニューを選んで、出てきた画面でSwift Fileテンプレート選択てのが一番基本なんですが、UIKit系の派生クラス用ならCocoa Touch Classテンプレートを使いましょう。

 

 

 Cocoa Touch Classテンプレートを選ぶと、次の画面ではこんな感じでUIViewを派生させて、MapViewクラス用意しますってのを指定する。

 

 その次の画面は、ファイルをどこに置くかの指定。プロジェクトフォルダ内が表示されてると思うので、そのまま保存でOK。そうすっとナビゲーションエリアにMapView.swift項目が追加されます。

 

 

 あらかじめUIViewの派生としてMapViewクラスの定義まで書いてくれてるので、あとは必要な処理として、作成時に地図画像用のサブ画面準備とか、ユーザーのタップに対する対応とかの関連作業をやっちゃえば、ViewController側は、そのMapViewを作ってself.viewに追加する程度で地図画面対応ができるわけっす。

 

        let mapView = MapView(frame:self.view.bounds)

        mapView.frame.origin.y = mapView.frame.maxY

        self.view.superview?.addSubview(mapView)

        UIView.animate(withDuration: 1) {

            mapView.frame.origin.y = self.view.bounds.origin.y

        }

 

ちなみに作ったMapViewは、self.viewじゃなく、self.view.superviewとして、self.viewの親UIViewに追加してます。

 こんな感じで、superviewプロパティには親側UIViewの参照が設定されとります。どこにもaddSubviewされてないUIViewの場合はsuperviewはnilになるんでオプショナルっす。オプショナルがわからん人は「アンラップしてチン♪」を読みましょう。親UIViewに追加する理由は次回。

 

 この例では、単に追加じゃなく、下から迫り上がるアニメーションでMapViewを追加表示してる。

 でもって、表示されたMapViewは、下に向けてのスワイプで画面を閉じるようにしました。こっちもアニメーション付き。

 

サンプル:

http://tetera.jp/xcc/book-sample/modalview.zip

 

 なんですが…

 実はAppleはこの手の画面切り替えをUIViewController間で行うことを推奨してまして…

 こんな感じで全面にマップ画面を出すなら、UIViewControllerを派生させてMapViewControllerなんかを定義して、これとViewController間で切り替えを行う、ってのがiOSアプリでの王道ってことになっとります。

 

 そこんところや、今回のUIViewの派生方法なんかは次回!

AD