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

 

 ご無沙汰。

 1ヶ月空いちゃったけど大丈夫!生きてます。

 

 iPhoneやiPadの画面サイズバリエーションがこれだけ増えた今、前回紹介したInterface BuilderでのAuto Layoutの指定技法は、iOSアプリ開発者必須教養と言えるもんなんですが説明始めるとキリないんですよ。

 なので、リブートキャンプではあんまり細かい解説はしません。

 キャンプ後始める予定のカメラでのキャプチャなんかで、その都度説明しようかと考え中。

 待ちきれんわー、な皆さんは、WWDCのビデオ見て学習しましょう。ビデオなので操作自体は、よくわかると思う(ただし説明は英語なりよ)。

 

WWDCのビデオ:

Auto Layout Techniques in Interface Builder

Best Practices for Mastering Auto Layout

Auto Layout by Example

Mysteries of Auto Layout, Part 1

Mysteries of Auto Layout, Part 2

 

 私の「親切すぎるiPhoneアプリ開発の本」を持ってる人は、ビデオを見る前に545ページのAuto Layoutの基礎や、756ページあたりから始まるInterface Builderでの設定を読んでおくとモアベターよ。Xcodeの進化でインターフェースは微妙に変わってるけど、基本となる情報は今でも通用するっす。

 

 

 コードで書くと、かなりな手間のAuto Layout(NSLayoutConstraint)の指定が、画面で効果を確認しながら図形アプリみたいに記述できる点がストーリーボードのいいところですな。

 ちなみに、Interface BuilderによるAuto Layoutの指定自体は.storyboardファイルに限らず、.xibファイルでもできるようになってます。

 

.xibファイル

 

 .xibファイルってのは、前回やったようなUIViewControllerのself.viewに対する「地図」ボタンの作成や配置とかをUIViewレベルでやるためのファイルっす。

 歴史的にはこの.xibファイルでUIViewを1つだけ用意して、それをUIViewControllerのself.viewに適用するってやり方があって、そこから発展してファイル自体にUIViewControllerのself.viewという概念を取り込んだ.storyboardファイルが生まれたわけですな。

 

 

 「UIViewとUIViewController」でMapViewController作るときに"Also create XIB file"てチェックがあったの覚えてるかな〜。

 

 

 あれをチェックすると、MapViewController.swiftファイルと一緒に、self.viewに対応するUIViewが一つだけ用意されてるMapViewController.xibファイルってのが作られるようになっているんです。

 昔は、こうやってUIViewControllerの派生クラスごとに対応する.xibファイルを作ってself.viewのレイアウトをInterface Builderで設定してた。

 なんだけど、今はストーリーボードが用意されて、1つのファイルに複数のビューコントローラを定義できるようになったんですな。

 つまりViewControllerに加えて、MapViewControllerもストーリーボードに追加できるわけです。

 

 

 MapViewController.swiftファイル作成時に、"Also create XIB file"をチェックしなくていいと書いた理由がこれ。

 

 でもってストーリーボードでは、ViewControllerからMapViewControllerへの遷移まで定義できるようになってる。つまりViewController.swiftに用意したshowAsViewメソッド

    @IBAction func showAsView() {
        let mapViewController = MapViewController()
        mapViewController.delegate = altDelegate
        self.present(mapViewController, //  mapViewControllerに切り替え
            animated: true)    //  切り替えはアニメーションさせる
    }

 て部分の大部分が、ストーリーボードでも定義できるわけですよ。

 てなわけで、ストーリーボードが無かった昔は、ビューコントローラごとに.xibファイルを用意してself.viewのレイアウトをやってたんですが、今はほとんどやりません。

注意)といっても、カスタムUIViewをプログラム中で作る時に、UINibを使って.xibファイルから生成なんてことに使えるので.xibファイルの存在意義は今でも十分あるんじゃないかと思われ。UINibについて興味ある人は自分で調べてみましょう。「親切すぎるiPhoneアプリ開発の本」を持ってる人は747ページあたりからの話ね。ちなみに.xibファイルは複数のUIViewのレイアウトを定義したりもできます。

 

 .xibファイルの説明が終わったところで、先に挙げたストーリーボードで複数のビューコントローラの定義、およびビューコントローラ間の遷移の定義ってのをやってみましょう。

 前回の最後のサンプルのMain.storyboardにMapViewController生成と呼び出しを加えてみるっす。

 

前回の最後のサンプル:

 http://tetera.jp/xcc/book-sample/storyboard-4.zip

 

 まずは「地図」ボタンを貼り付けた要領で、ライブラリからView Controllerをcanvasビューの何もないところにドラッグ&ドロップっす。今回の場合はoutlineビューにドロップしてもいいです。

 

 

 新しく貼り付けたView Controllerを選んで、IdentifyインスペクタのClassを見てもらうと、薄い文字で"UIViewController"ってなってます。

 

 

 こいつを"MapViewController"に書き換えてください。

 

 

 MapViewControllerはUIViewControllerからの派生なので、こういった切り替えができる。

 次に、outlineビュー側のViewController項目の中にある「地図」ボタン項目をcontrolキーを押しながらクリックしてドラッグすると、@IBActionメソッドを設定した時みたいに線が伸びるので、この線をMap View Controller項目(classをMapViewControllerに変更したので項目名も変わった)まで持っていって、そこでボタンを放しましょう。

 

 

注意)canvasビュー上でやってもいいし、canvas、outlineビューをまたがってもいい。ここではcanvasビューを縮小表示しているために、canvasビュー上での線を引き出す操作がやりにくいのでoutlineビューを使ってる。

 

 ↓こんな感じでoutlineビューからcanvasビューに線を伸ばしてもいい。

  canvasビューではMap View Controllerのアイコンじゃなくてself.viewが反応する点に注意。

 

 

 そうすっとAuto Layoutの指定の時みたいにメニューがポップするんで、Present modaly項目をクリックしましょう。

 

 


 これで「地図」ボタンがタップされるとMapViewControllerに遷移する以下のコードを記述したのと同等となる。

    
        let mapViewController = MapViewController()
        self.present(mapViewController, //  mapViewControllerに切り替え
            animated: true)    //  切り替えはアニメーションさせる

 

 

 ちなみに、この記述はcanvasビューでは連結線として表現され、outlineビューでは項目として表示されます。

 

 

 これはSegue(セグエ)と呼ばれ、UIViewController間の遷移をオブジェクトとして表現するものです。

 

Segue

 セグエ: (ある曲・話題・状態などから)円滑に[穏やかに]移行する

 

 これで「地図」ボタン押されるとMapViewControllerに遷移することになるんだけど、前回設定した「地図」ボタンタップによるViewControllerのshowAsViewメソッド呼び出す仕組みも残ったままなんすよ。

 このままだと「地図」ボタン押されると、2系統でMapViewControllerに遷移することになるんで変な感じなります。

 Runして「地図」ボタンタップして確認してみてちょ。

 下のDebugエリアのコンソールにワーニングが出るはず。

 

サンプル:

 http://tetera.jp/xcc/book-sample/storyboard-5.zip

 

 

 これは、最初のMapViewController作って切り替えて画面表示の最中に、もう一度別のMapViewController作って、そっちの画面に切り替えようとしたのを怒られてる状態。

 「今、画面切り替えてる最中でしょ」って怒られてるわけです。

 これを防ぐためには、セグエじゃない方、前回の「地図」ボタンタップ側のターゲットアクション実行を解除してやればいい。

 

 ここで、ちょっと面白い実験。

 本来、Main.storyboard側で「地図」ボタンタップ側のターゲットアクションを解除し、使わなくなったViewController.swift側のshowAsViewメソッドを削除するのが正当なんですが、そうせずにViewController.swift側のshowAsViewメソッドだけを削除しちゃうとどうなるか。

 ストーリーボード側の「地図」ボタンタップ側のターゲットアクションには、まだshowAsViewメソッドが設定されたままなんですが、この状態でRunさせて「地図」ボタンをタップするとどうなるか〜。

 

  ・・・
  class ViewController: UIViewController,
  ・・・
    @IBAction func showAsView() {
        let mapViewController = MapViewController()
        mapViewController.delegate = altDelegate
        self.present(mapViewController, //  mapViewControllerに切り替え
            animated: true)    //  切り替えはアニメーションさせる
    }

 

 Runして「地図」ボタンタップして試してください。

 はい死んだ。

 

サンプル:

 http://tetera.jp/xcc/book-sample/storyboard-6.zip

 

 確認したら、Stopボタン押してアプリを強制停止しちゃってください。

 

 

 ま〜、ちゅうわけで@IBActionメソッドを削除する場合は、ストーリーボード側のターゲットアクション接続も確実に切りましょう。

 接続を切るにはMain.storyboardで「地図」ボタンを選んでConnectionsインスペクタでSent EventsグループのTouch Up Insideのターゲットアクション接続の×印をクリックするだけっす。

 

 

 ストーリーボードいじってて、Runするとアプリが死ぬようになったら、真っ先にここら辺を疑うといいです。まあ、そういう時もDebugエリアのコンソールに何かメッセージが出てないかをちゃんと確認するのが一番先だけどね。

 

 ちなみに、ストーリーボードで接続切って、swiftソース側の@IBActionメソッドを残しておくのは問題ない。単に呼び出されないだけっす。

 Runして「地図」ボタンをタップ。

 

サンプル:

 http://tetera.jp/xcc/book-sample/storyboard-7.zip

 

 

 セグエ側の接続が残ってるので、ちゃんと地図画面が表示されます。

 ただし、現時点では問題が残ってる。

 地図画面タップして気づいた人もいると思うけど、「そして委譲へ」で実装したDebugエリアのコンソールにタップ位置の出力が実行されないんすよ。

 

 

 ていうのもセグエでは、showAsViewメソッドでやってたようなMapViewControllerのdelegateプロパティに、ViewControllerのaltDelegateオブジェクトが設定されてないため。

        let mapViewController = MapViewController()
        mapViewController.delegate = altDelegate ←これね
        self.present(mapViewController, //  mapViewControllerに切り替え
            animated: true)    //  切り替えはアニメーションさせる

 

 セグエはUIViewController間の遷移はやってくれるんですけど、ViewControllerとMapViewController間というようなプログラマが勝手に拡張したやり取りまでは面倒みてくれないんすよ。

 なのでMapViewControllerのdelegateプロパティへのaltDelegate設定はプログラムで責任持って実装しないといけません。でもセグエ、こっちの知らないところで勝手に遷移しちゃうじゃん。どーする?

 そういう時のために、UIViewControllerにはセグエでの遷移直前に呼び出されるメソッドが用意されてます。

    func prepare(for segue: UIStoryboardSegue, sender: Any?)

てメソッドなんですが、こいつを今回の遷移元であるViewControllerでオーバーライドすることで、セグエ時のaltDelegateの設定が可能になります。

 セグエの情報はUIStoryboardSegueのインスタンスとして表現されてて、このオブジェクトのdestinationプロパティから遷移先のビューコントローラのインスタンスが手に入るんですな。

 なので、ViewControllerクラスで次のようにprepareメソッドをオーバーライドで追加してaltDelegateを設定します。

 オーバーライドがわからん人は「こんにちは世界」を読みましょう。

  ・・・
  class ViewController: UIViewController,
  ・・・
    //  セグエの遷移直前
    //  segue.destinationがMapViewControllerなので、型変換して
    //  altDelegateを設定する
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        //  今はセグエの遷移先はMapViewControllerのみだが
        //  この先、いろいろなビューコントローラに遷移する可能性を
        //  考えて、if letによるチェックを入れる
        if let mapviewController = segue.destination as? MapViewController {
            mapviewController.delegate = altDelegate
        }
    }
  ・・・ 

サンプル:

 http://tetera.jp/xcc/book-sample/storyboard-8.zip

 

 これでRunして地図画面でタップすると、以前のようにタップ位置がコンソールに表示されるようになる。

 ViewControllerの派生元であるUIViewControllerのprepareメソッドは空(クイックヘルプに書いてる)なので、super側のprepareメソッドを呼び出す必要はないです。呼ぶだけ無駄。

 以上、モーダル遷移をストーリーボードで定義する方法でした。

 

 こうなると、ストーリーボードを使い、ViewControllerのself.viewに「地図」ボタンを貼り付けたように、MapViewControllerのself.viewにMapViewを貼ったりしてMapViewControllerのviewDidLoadメソッドでの作業を軽減させたくなるのは人情なわけですが、ここで気をつけないといけないのは、使ってるMapViewクラスがUIViewをカスタマイズしていて作成時に子供ビューを自身に貼り付けてる点。

 ライブラリに用意されてるのはUIViewなので、これを「地図」ボタンのようにself.viewに配置してクラスをMapViewに書き換えるのは基本なんだけど、それだけだと予想外の結果になります。

 ま、興味のある人は自分なりの解釈で実際にやってRunしてみてください。

 

 ここら辺をどう解決するかは次回。