Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 272 - Main.storyboardでマルチMVCを設定

2017年1月17日:チュートリアルを調整中

Psychologistプロジェクトの続きです。以前完成させたHappinessプロジェクトを使用するのでプロジェクトをオープンします。

プロジェクト・ナビゲータにあるHappinessViewController.swiftfaceView.swiftの2つのファイルを選択して、Psychologistプロジェクトのプロジェクト・ナビゲータにドラッグ&ドロップします。

f:id:yataiblue:20150408112113j:plain

ダイアログが出現するのでアイテムをコピーします。すると、プロジェクト・ナビゲータにコピーした2つのファイルが並びます。

f:id:yataiblue:20170117165226j:plain

ストーリーボードにあるべきHapinessViewControllerはどうしたらいいのでしょう?

これがまた簡単なんです。

ストーリーボードにあるHappinessViewControllerをコピペするたけです。

f:id:yataiblue:20170118090157j:plain

するとストーリーボードに2つのViewContorollerが並びました!*1

複数のMVCを繋げるためにSpritViewControllerを使用します。オブジェクト・ライブラリからSplit View Controllerを選択して、storyboardにドラッグ&ドロップします。

f:id:yataiblue:20170118085808j:plain

このテンプレート・クラスには、複数のMVCテンプレートが尻尾のように付いていますが、すべて削除して、Split View Controllerだけ残します。

次にSplit View ControllerからPsychologist View Controllerに「Ctrl + ドラッグ」してリリースするとポップアップウインドウが現れます。

f:id:yataiblue:20170117172229j:plain

ここでSegueの設定をします。「master view controller」を選び、PsychologistViewControllerをマスター設定にします。

同様にSprit View ControllerからHappyViewControllerに「Ctrl + ドラッグ」してポップアップウインドウから「detail view controller」を選びます。

iPadをターゲット*2にして、ランで確かめるのですが、PsychologistViewControllerに「Is Initial View Controller」の矢印がPsychologistViewControllerに入っているのでSplitViewControllerにドラッグして動かします。

f:id:yataiblue:20170118103543j:plain

これでランするとiPadで問題無くHappinessViewControllerとPsychologistViewControllerが表示されます。

f:id:yataiblue:20170118104218j:plain

しかし、ターゲットデバイスをiPhon7(iPhone7plusもちょっと異なる)にすると見えなくなります。

f:id:yataiblue:20170118104259j:plain

SplitViewControllerはiPhoneでサポートされていないからです。これを解決するためにNavigation Controllerを組み込みます。

PsychologistViewControllerを選択した状態で「>Editor」 -> 「Embed In」 -> 「Navigation Controller>」を選びます。

f:id:yataiblue:20170117151748j:plain

すると、iPhoneでも問題無く表示されるようになりました。

f:id:yataiblue:20170118105933j:plain

ここで、PsychologistViewControllerに設置しているボタンを押しても何も起こりません。

Segue(セグエ)が設定されていないからです。

Segueとは?

MVCを繋いで表示することができても、内容をコントロールすることはできません。描画をコントロールするのが「Segue」です。

4種類のSegueがあります。

  1. Show
  2. Show Detail
  3. Present Modally
  4. Present As Popover

ストーリーボード上で「Ctrl + ドラッグ」でSegueをつなぐ場合、以下のように一部「deprecated(不採用)」な古いSegueが残っていますが、それは無視します。

f:id:yataiblue:20170121152550j:plain

「Show Detail」は「Split View Controller」のDetailを表示するとき、もしくは「Navigation Controller」でpushしてViewControllerを表示する時に使います。

ます、Segueの理解で重要なのところは、新しいMVCインスタンスが作られるというところです。既存のMVCに変化を加える訳ではありません。Segueが呼ばれると古いMVCは消され、新しいMVCが作られることで変化が見えるんですt*3

じゃあどうやってSegueを使うのでしょう?

ストーリーボードでSegueのインスタンス

ViewControllerからViewControllerへ「Ctrl + ドラッグ」すればいいんです。

ポップアップウインドウが出現するのでSegueを選べばいいだけです。

こうしたらUIStoryboardSegueクラスインスタンスがPsychologistViewControllerのプロパティ(一般的にVIewControllerのプロパティ)として作られます。ここで新しいMVCを作るためにIdentifierを設定する必要があります。

コードでSegueのインスタンス

UIViewControllerが持っている次のメソッドを使います。

func performSegue(withIdentifier identifier: String, 
                                     sender: Any?) {
    // code
}

何度も言いますが、「Ctrl + ドラッグ」でSegueを設定する場合、このメソッドは必要ありません。

ストーリーボードでセグエをインスタンス化する場合、このコードを知る必要は無いのですが、iOSデベロッパーとしてセグエ(Segue)の制御するコードを知っておくべきです。

UIViewControllerから、このメソッドにidentifierを引数として渡し、Segueを作るのですが、senderは基本的になんでもかまいません。このSegueを発動させるオブジェクトになるので、一般的にUIButtonオブジェクトになるでしょう。しかし、ボタンでなくてもかまわないのでAnyでいいわけです。

セグエ(Segue)がインスタンス化したら後は共通

新しいMVCを作り出すために、UIViewControllerが持っている次のメソッドにSegueインスタンスを引数として渡す必要があります。

func prepare(for segue: UIStoryboardSegue, 
                sender: Any?) {
    // code
}

このメソッドが呼ばれると、segueを持つプロパティ「destination」を使ってMVC(UIViewController)が利用できます。

func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    
    if let identifier = segue.identifier {
        switch identifier {
        case "Show Detail":
            if let vc = 
             segue.destination as? DetailViewController {
                vc.propaerty1 = ...
                vc.callMethodToSedItUp(...)
            }
        default: break
        }
    }
}

segueの基本でした。

Happinessプロジェクトに戻る

ストーリーボードでSegueインスタンスを作ります。

PsychologistViewControllerにあるボタン「黄金の蛇」を「Ctrl + ドラッグ」でHappinessViewControllerの上でリリースします。。

Segueメニューウインドウが出現するので、「Show Detail」を選びます。

これだけです。これでSegueインスタンスができるんです。

f:id:yataiblue:20170121153828j:plain

PsychologistViewControllerとHappinessViewControllerの間にSegueができるので、これを選択してアイデンティティ・インスペクタを選び、identifireに「sad」と入力します。

同じように「踊る樹」ボタンのidentifierを「happy」にして、「のろまな亀」ボタンは「meh」にします。

ランして確かめるとSegueインスタンスが存在するので画面の切り替わりは生じます*4。しかし、まだ新しいMVCを作りだしていないので表情は変わらないんです。

表示を変化させるために「prepare」メソッドを使うんです。

Segueを制御するprepareメソッドはUIViewControllerが持っているので、PsychologistViewControllerクラス宣言内に書きます。

「prepar〜」とタイプしていくと自動的に「prepare(for segue: UIStoryboardSegue, sender: Any?)」が候補に出現するので「tab」キーで選択すると、「override func」というキーワードが下記のように自動的に付けられます。

override func prepare(for segue: UIStoryboardSegue, 
                         sender: Any?) {
      code
}

ここのコードを次のように変更します。

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

    if let hvc = segue.destination as? HappinessViewController {
        if let identifier = segue.identifier {
            switch identifier {
            case "sad": hvc.happiness = 0
            case "happy": hvc.happiness = 100
            default : hvc.happiness = 50
            }
        }
    }
}

このメソッドは、2つのパラメーターを受け取ります。まず最初の「segue」は、UIStoryboardSegueクラスで、これはUIViewControllerクラスのインスタンスをプロパティ(destination)として持っています。2番目の「sender」は、何でもかまわないんです。フィールドでもイメージでもいいので「Any?」になります。ここではボタンになっています。しかし、segueでMVCを作ったときに必要ありません。

segueのプロパティ「destination」は、新しく作られるUIViewControllerのことで、ここでは明らかにHappinessViewControllerを示しています。HappinessViewControllerにキャストする必要があります。

どのsegueが呼ばれたのか判断するのが「identifier」なので、「switch-case」文を使って、hvcのプロパティ「happiness」を変更します。

これだけです。これでハッピーフェイスの表情を変えられるです。

じゃあここでランします...

セグエを呼んだらランタイム・エラー!

このブログエントリーで遭遇したエラーが出現します。

なぜでしょう? 実はスタンフォード大学のポール先生の講義を聞けば直ぐ理解できます。ポール先生は、ここでわざとランタイム・エラーを発生させていたんです。

実は、Happinessプロジェクトのコードに問題があったんです。

hvcの持っているプロパティ「happiness」の値を変更しました。HappinessViewControllerに宣言しているプロパティ「happiness」にはプロパティ・オブザーバを使っています。値の変更があったら「updateUI()」カスタムメソッドを呼んで、画面の書き換えをします。updateUI()のコードをみると次のようになっています。

func updateUI() {
    faceView.setNeedsDisplay()
}

これが「nil」のためsetNeedsDisplay()メソッドが呼べないのでランタイム・エラーになりました。

じゃあfaceViewプロパティをみると、これが@IBOutletの枕詞がついているオプショナルプロパティでした。happinessプロパティが呼ばれた時にまだfaceViewプロパティがインスタンス化されていないためエラーになるということです。

ということで次のようにオプショナルに変更します。

func updateUI() {
    faceView?.setNeedsDisplay()
}

これで問題なく画面の遷移が生じました!iPad Airランドスケープモードにして確認。

f:id:yataiblue:20170124162301j:plain

スタンフォード大学iOS開発講義の進行は、開発指向を体現しながら進めているように思えます。Sprit View Controllerを使ってMaster View ControllerとDetail View Controllerに分けたのですが、今のところMaster View ControllerにNavigation Controllerを使って上部にタイトルを表示させているのですが、Detail View Controllerにタイトル表示がありません。

じゃあタイトルを表示させようっていうのが次のポイントです。

もう分かりますね。HappinessViewControllerが選択された状態で「>Editor」 -> 「Embed In」 -> 「Navigation Controller>」を選びます。すると次のようにNavigation Controllerが組み込まれます。

f:id:yataiblue:20170124164951j:plain

これでランしたらボタンが動かなくなります。そりゃそうですね。遷移先がHappinessViewControllerからNavigationControllerに変更されたからです。

これをコードで修正していかなければなりません。segue先がNavigationControllerになっているのでワンクッション用意させる必要があります。

var destination:UIViewController? = segue.destination

HappinessViewControllerもNavigationControllerも共通のスーパークラスUIViewControllerのサブクラスになるので、セグエで生成する「destination」をタイプキャスト可能にするため「?」を使ってオプショナルにします。

ややこしいんですが、「desitination」は推移先のUIViewControllerクラスインスタンスを意味していますが、、UINavigationControllerのことではなく、そのプロパティvisibleViewControllerで表示するUIViewControllerのことなのでタイプキャストが必要になります。

if let navCon = destination as? UINavigationController {
    destination = navCon.visibleViewController
}

destinationはHappinessViewControllerクラスの事なのでキャストさせてhvcインスタンスを作り、segueのidentifierを使って、hvcのhappiness値を変更します。

if let hvc = destination as? HappinessViewController {
    if let identifier = segue.identifier {
        switch identifier {
        case "sad": hvc.happiness = 0
        case "happy": hvc.happiness = 100
        default : hvc.happiness = 50
        }
    }
}

これをすべてまとめると

override func prepare(for segue: UIStoryboardSegue, 
                         sender: Any?) {
        
    var destination:UIViewController? = segue.destination
        
    if let navCon = destination as? UINavigationController {
        destination = navCon.visibleViewController
    }
        
    if let hvc = destination as? HappinessViewController {
        if let identifier = segue.identifier {
            switch identifier {
            case "sad": hvc.happiness = 0
            case "happy": hvc.happiness = 100
            default : hvc.happiness = 50
            }
        }
    }
}

この講義を最初に聞いたとき、ポール先生のコードがすっきりしていなかったのでもやもやした気分でした。destinationをUIViewControllerにしたと思ったらNaigationControllerにキャストし直し、またUIViewControllerに戻したと思ったらHappinessViewControllerにキャストする... しかし、今この講義を聞き返して感じたことは、プログラミングを開発している過程の中で思考の変化に合わせてコードを改変している結果なんだと。まさに、プログラマーの思考の断片を垣間見たんだと思いました。

今の私が同じようなコードを書くとしたら以下のようになります。どうでしょう?

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

    guard let destination = 
            segue.destination as? UINavigationController else {
        return
    }
    guard let hvc = 
            destination.visibleViewController as? 
                HappinessViewController else {
        return
    }
    guard let identifier = segue.identifier else {
        return
    }
    switch identifier {
    case "sad":
        hvc.happiness = 0
    case "happy":
        hvc.happiness = 100
    default:
        hvc.happiness = 50
    }
}

それにしても、プログラマーの頭の回転は速いですね。ついていけません...(T_T)

ナビゲーションアイテムのタイトルを変更するために、「hvc.navigationItem.title = "\(identifier)"」を加えるという方法もあります。

*1:コピペされた直後のfaceViewには、@IBDesignableによるプレビューは表示されません。他のコードをクリックすれば表示されます。

*2:iPhoneをターゲットにするとレイアウト上の制限があるので、ますiPadで確かめます。

*3:unwind segueは既存のMVCを消して前に戻すのでちょっと挙動が異なります。

*4:iPadでは実際切り替わったようには見えませんか、iPhoneで切り替わりが見えます。