woshidan's loose leaf

ぼんやり勉強しています

1.4 UIViewController2 ModalViewController (storyboard) 作業的なこと

内容

先にModalで行われる処理の概要を説明して後で、作業を各個撃破する方針でまとめ直しました。

長くなったのでDelegateパターンとかViewControllerの親子関係とか知識的なことは前記事に回しました。

  • 作業的なこと
    • モーダルを表示するための作業
      • storyboardを利用してデフォルトのsegueを追加する
      • パーツアクションに対応したメソッドからsegueを呼び出す
      • 親コントローラ(UIViewController)からModalに表示するデータを与える
      • segueを使わないでモーダルを呼び出す場合
    • モーダルを閉じるための作業
    • 表示してからモーダルを閉じるまでの一連の流れの確認
    • 演習をやってみて

作業的なこと

前提として1.3のサンプルプロジェクトを1.3の演習が終わった状態で使用するとします。

また、元の資料では、モーダルを呼び出すViewControllerは、"MixiSampleViewController"ですが、ここでは"MyUIViewController"とし、新しく作成するViewControllerのサブクラスの名前は"MixiSecondViewController"のかわりに"MyModalViewController"とします。

大した意味は無く、そちらのほうが自分にとってクラスの役割が分かりやすいと思ったからです。

それでは、追加で新しいViewControllerのサブクラス"MyModalViewController"を作成します。

モーダルを表示するための作業

storyboardを利用してデフォルトのsegueを追加する

実際の作業に移る前にsegue(セグエ)についてまとめます。segueは二つのシーン(ViewControllerで管理している一単位)間の遷移方法に関する設定を扱うオブジェクトです。

Androidでいうとインテントに少しだけ近い気がします。

主に、storyboard上で遷移を決めるときに利用します。

ためしにsegueを追加して、UIButtonを押したらMyModalViewControllerをModalとして表示する処理を作ってみます。

MyUIViewController上のUIButtonをcontrolボタンをクリックし、MyModalViewControllerまでドラッグします。

Action Segue
  show
  show detail
  present modally
  popover presentation
  custom
Non-Adaptive Action Segue
  push (deprecated)
  modal (deprecated)

というような内容のパネルが出てくるので、ドラッグ先のModalViewControllerをどのように表示したいか選択しましょう。

今回はModalとして表示させたいのでpresent modallyを選択します。

MyUIViewControllerからMyModalViewControllerへ線が出ています。

MyModalViewControllerをModalとして表示する処理のための作業はこれで完了です。

パーツアクションに対応したメソッドからsegueを呼び出す

segueはそのままでも非常に便利ですが、たとえば、次のViewControllerを呼び出す前に、そこで使うデータの読み込みを済ませておきたいから、データの読み込みを待ってから次のViewControllerを表示させたい、という風に、任意のタイミングでsegueを実行したいこともあります。

その場合は、パーツアクションに対応したイベントハンドラをUIViewControllerに用意して、そのイベントハンドラの中でsegueを利用します。

segueをコードの中で利用するためには、segueの識別子と指定する必要があります。storyboard上でsegueをクリックしてください。 先ほどつないだときに出た線のことです。

あるユーティリティのAttribute Inspectorという部分のStroryboard Segue Identifierという項目にそのsegueの識別子を入力します。

今回は識別子は"presentMyModalViewController"としましょう。

新しくUIButtonを作って、そのボタンにイベントハンドラを用意しましょう。名前はmodalButtonTappedとします。

とりあえず、

- (IBAction)modalButtonTapped:(id)sender {
}

と書いておきます。storyboard上で結びつけもしましょう。

次に、このイベントハンドラの中で、performSegueWithIdentifier:sender:メソッドを使います。これは、UIViewControllerのインスタンスメソッドです。第一引数にsegueの識別子を指定し、二つ目の引数のsenderはsegueに設定された内容を実行させようとしている(=メッセージの送り手である)UIViewControllerのクラスのインスタンスを入れます。

selfを入れることが多いそうです。この場合だとself = MyUIViewControllerのインスタンスです。

- (IBAction)modalButtonTapped:(id)sender {
    [self performSegueWithIdentifier:@"presentMyModalViewController" sender:self];
}

新しく追加したボタンをクリックして画面が遷移することを確認します。

親コントローラ(UIViewController)からModalに表示するデータを与える

segueを用いて画面遷移を行う際に、表示したいViewControllerに何かデータを渡したい場合は、UIViewControllerのインスタンスメソッド-prepareForSegue:senderを利用します。

このメソッドはUIViewControllerのサブクラスを作った際に自動的にコードスニペットが.mファイルにコメントアウトされた形で記述されています。

必要な場合は、コメントアウトを外して実装してみましょう。

-prepareForSegue:senderの引数は実行されるsegueとsenderです。

画面遷移は1つの画面に対して複数存在することがあり、同様にsegueも複数存在することがあります。

そのため、どのsegueを用いるどの画面遷移かを見分けて処理を行う必要がありますが、それには、segue.identifierプロパティを利用します。

また、処理にあたって遷移先のViewControllerが必要な場合はsegue.destinationViewControllerで取得することが出来ます。

サンプルコードを元の資料から引用して確認してみます。

#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.

    if ([segue.identifier isEqualToString:@"presentMyModuleViewController"]) {
        UIViewController *destination = segue.destinationViewController;
    }

}

segueを使わないでモーダルを呼び出す場合

これまでは、segueを使ってモーダルを呼び出して表示する方法について説明していましたが、iOS4以前はstoryboardがなく、それに従ってsegueを利用することも出来ませんでした。

ここでは、storyboardを使いつつ、ただsegueを利用しない、という方法について簡単に紹介します。

まず、表示される側のMyModalViewControllerに、Storyboard IDという識別子をつけましょう。

右側のユーティリティの上側、右から3つめのアイコンのタブの中にStoryboard IDの設定項目があるので、ここでは、例えば"MyModalViewController"とします。

次に、呼び出すモーダルのViewControllerを生成するメソッドを書きましょう。UIStoryboardのインスタンスメソッドinstanceViewControllerWithIdentifier:(引数は生成したいViewControllerのStoryboard ID)を利用します。

このメソッドを呼び出すとき、引数のViewControllerのインタフェースファイルをUIViewController.mにインポートするのを忘れないでください。

最後に生成したViewControllerをpresentViewController:animated:completion:メソッドを使って表示させます。このメソッドの1つ目の引数に表示させたいViewControllerを与えます。

最終的にコードはこんな感じです。

- (IBAction)modalButtonTapped:(id)sender {
    MyModalViewController *modalViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"MyModalViewController"];
    [self presentViewController:modalViewController animated:YES completion:nil];
}

めでたくモーダルを表示させることができたはずなので、今度はモーダルのViewController上のボタンを押したらモーダルを閉じる(非表示にする)処理のための作業について確認します。

モーダルを閉じるための作業

モーダルを閉じる処理を実行する際には、呼び出し側のUIViewControllerをdelegate(派遣団員)、表示される側のModalViewControllerをdelegation(代表団)とするObjective-CにおけるDelegateパターンに沿って実装を行うのでした。

また、このDelegateパターンでは、delegateとdelegationの間でインタフェースのようなものにあたるプロトコルを、delegation側のクラスに定義し、delegate側のクラスに実装します。そして、delegation側にはそのプロトコルのオブジェクトへの参照(弱参照)をプロパティとして追加しておくのでした。

この流れに沿って、具体的なコードや手順についてまとめておきます。

見出しのクラス名は前回の記事にあわせていて、コード内については上からのコードと同一の名前になっています。

モーダルを閉じるメソッドを定義したプロトコルをModalViewControllerに宣言

プロトコルは~Delegateという名前で終了します。

// MyModalViewController.h

#import <UIKit/UIKit.h>
@protocol MyModalViewControllerDelegate <NSObject>
- (void)modalViewControllerButtonTapped;
@end

@interface MyModalViewController : UIViewController
@end

...

UIViewControllerのインタフェースファイルの@interface部に準拠するプロトコルを宣言

クラスが準拠するプロトコルを宣言するためには、クラスの宣言の際に

@interface クラス名 : スーパークラス名 <準拠するプロトコル名>

とします。なので、この場合、

// MyUIViewController.h
#import "MyModalViewController.h" // プロトコルを書いた.hファイルをインポートすること

@interface MyUIViewController : UIViewController <MyModalViewControllerDelegate>
// 中略
@end

となります。

UIViewControllerの実装ファイルでプロトコルメソッドを実装

プロトコルに書いてある(void)modalViewControllerButtonTappedを実装して、モーダルを閉じるための動作を書きましょう。

instantiateViewControllerWithIdentifierをつかっているので、設定していなければ、MyModalViewControllerのStoryboard IDを設定していなければstoryboardで設定してください。

その際、Use Storyboard IDにチェックを入れるのを忘れないで下さい。

@implementation MyUIViewController

- (IBAction)modalButtonTapped:(id)sender {
    MyModalViewController *modalViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"MyModalViewController"];
    modalViewController.delegate = self;
    [self presentViewController:modalViewController animated:YES completion:nil];
}

- (void)modalViewControllerButtonTapped
{
    [self dismissViewControllerAnimated:YES completion:nil];
}
@end

1つ目のメソッドは、ボタンがタップされて、MyModalViewControllerのインスタンスを生成してモーダルを表示する時のメソッドです。

プロトコルの準拠ではありませんが、このとき、呼び出すMyModalViewControllerのインスタンスdelegateプロパティにselfを代入しています。Delegateパターンではdelegationに当たるクラスのインスタンスdelegateのクラスのインスタンスへの参照をプロパティとして持つのでした。

二つ目がプロトコルに宣言されているメソッドの実装です。dismissViewControllerAnimated:completion:メソッドでモーダルの削除をします。

ModalViewController.hのプロパティにプロトコルのオブジェクトの参照を追加

// ModalViewController.h
@interface MyModalViewController : UIViewController
@property (nonatomic, weak) id<MyModalViewControllerDelegate> delegate; // <- 追加
@end

元の資料からポイントを引用しておきます。

  • プロパティのタイプが weak になっている点。これは循環参照を避けるためにあります。
  • id という型。これは MyModuleViewControllerDelegateに準拠したid型となります。つまりMyModuleViewControllerDelegateに準拠していたらどんなクラスの参照でもOKということです。

ModalViewControllerのイベントハンドラ内にモーダルを閉じるためのメソッドを追加

// ModalViewController.m
- (IBAction)buttonTapped:(id)sender {
    [self.delegate modalViewControllerButtonTapped];
}

表示してからモーダルを閉じるまでの一連の流れの確認

さて、これで正しくモーダルを表示してから閉じることが出来るようになりました。

元資料のクラス名を入れ替えただけですが、最後にもう一度流れを確認します。

  1. MyUIViewControllerのmodalButtonTappedが呼ばれる
  2. MyModuleViewControllerインスタンスを生成し、delegateプロパティに self (MyUIViewControllerインスタンス) を代入する
  3. [self presentViewController:modalViewController ... でMyModuleViewControllerを表示する
  4. MyModuleViewControllerのボタンをタップすると、タップハンドラ (-buttonTapped) が呼ばれる
  5. MyModuleViewControllerの self.delegate すなわち 2.でセットした MyUIViewControllerインスタンスメソッド -modalViewControllerButtonTapped が呼ばれる
  6. MyUIViewControllerのmodalViewControllerButtonTapped内で [self dismissViewControllerAnimated... を呼ぶことでモーダルが閉じる

演習をやってみて

  • ButtonやLabelを.hに書いたプロパティや.mに書いたメソッドに結びつけるとき、ViewControllerのところをクリックしないと行けないのがしばらく気づかなくて焦りました
  • 第2回 iOS Training - connpass のリンクからいける資料だと記述するべきファイル名、インポートしておくべきヘッダファイルなどが見当たらなかったので、右側のリンクから行ける旧版の資料で補完してました
  • 旧版の資料を見て、やっぱりプロトコルは補完的な役割なんだなぁ、と思いました
  • ViewControllerの中のViewのところをいじりたいとき(たとえばButtonから矢印伸ばしてセグエ使いたいとき)、Viewの部分がやたらと拡大されて、他のViewControllerが見えなくなってしまいます。だけど、ピンチアウトして縮小してしまうと、Viewの選択が解除されて困っています。操作難しい。。