2012年6月26日火曜日

iOS Storyboardによる画面遷移の実装(Navigation Controller)

iOSアプリケーションでは比較的使うことが多いと思われるNavigation Contorollerですが、storyboardを使ってNavigationControllerの遷移を実装してみる。
今回の目標:Navigation Controllerによる画面遷移をstoryboardで作る
なぞなぞを表示して、その回答の選択肢がボタンになっている。ボタンを押すことで画面遷移し、回答が正解かを画面上に表示するだけのサンプルプログラムを作成する。
プロジェクトの作成
前回のプロジェクト(iOS Storyboardによる画面遷移の実装(Modal View))をそのまま流用します。
NavigationControllerの追加
「Single View Application」から作成した前回のプロジェクトにNavigationControllerを追加する。 NavigationControllerの追加はオブジェクトライブラリ(右下のやつ)からドラッグしても作成出来ますが、ドラッグして作成すると、なぜかTableViewがもれなく付いてくる。 xcodeでは挿入が出来るのでその機能を使います。挿入したいViewControllerを選択して、メニューのEditor - Embed in- Navigation Controller を選択するとNavigationControllerが挿入される。
メニューからEditor - Embed in- Navigation Controller を選択。
NavigationControllerが挿入される。
なぞなぞ用の新しいViewControllerの追加
なぞなぞ表示用に新しいViewControllerを追加して、Navigation Barを表示にしておく。 そして、一番初めの画面になぞなぞを画面に遷移するためのボタンを新しく作成する。 新しく作成したViewController用にカスタムクラス(RiddleViewController)を作成し、カスタムクラスとして設定しておく。
Navigation Barを表示。
新しいボタン作成。
カスタム用の新規クラス。
カスタムクラスとして設定する。
「なぞなぞ」ボタンが押されたらなぞなぞ画面に遷移させる。「なぞなぞ」ボタンをCntrollを押しながら遷移先の画面にドラッグし、セグエとして「Push」を選択する。また、分かりやすいようにタイトルも入れておく。
「Push」を選択。
=== [2014/10/05 追記] ===

Xcode6ではSegue名が変更になっています。詳しくはXcode6ではStoryboadで指定するSegue名が変更されている(https://selection9.blogspot.jp/2014/10/xcode6storyboadsegue.html)を参照下さい。

=== 追記終わり ===

タイトルの編集。
なぞなぞ画面の作成
なぞなぞを表示し、その結果を入力するボタンを3つ作成する。もちろん、正解は「しか」。
なぞなぞの回答を表示する画面を作成する。そして、追加ViewControllerのカスタムクラス(AnswerViewController) を作成し、設定する。
カスタムクラスとして設定。
回答ボタンを押した際のSegue(セグエ)を作成する。
ボタン一つ一つに対応したSegue(セグエ)を作成しても良いが、回答表示用のViewControllerは共通の画面を使用 したいので、今回はViewController間のSegue(セグエ)を作成する。(図参照) 画面の下の方にある丸っぽいのがViewControllerを表しているので、そこからControllボタンを押しながら 遷移先のViewControllerまでドラッグする。Segueのスタイルは「Push」を設定する。
回答用画面の編集
目印のためタイトルを入れて、回答結果を表示するラベルを設置。ラベルの内容は後で回答結果に伴い変更するので、 このままで良い。
アウトレット、アクションの作成
回答用の画面に設定したラベルのアウトレットを作成する。
※このアウトレットは無理に必要無いです。後で直接ラベルのテキストを変更しようとこの時は考えていたのですが 画面の初期化順の影響でうまく動かなかったので。


なぞなぞ画面に配置した回答用のボタンのアクションを作成する。各ボタンをControllボタンを押しながらこの画面のカスタムクラスであるRiddleViewController.mにドラッグしアクションを作成する。 これを3つのボタン分行う。因みに、ヘッダでなくボディファイル(拡張子mのファイル)に作成しているのはこのメソッドがプライベートなメソッドだからです。ボタンのイベントを受けられれば良いので、このメソッドを外部に公開する必要は無いことからプライベートにしている。
Segue(セグエ)にIDを付ける
なぞなぞ画面から回答結果の画面へ遷移するSegueのIdentifireを設定する。この値は後でCode上から指定することになる。
ソースコードの編集
RiddleViewController.mの内容。
#import "RiddleViewController.h"
#import "AnswerViewController.h"

@interface RiddleViewController ()
@property (nonatomic) NSString* answer;
@end

@implementation RiddleViewController

@synthesize answer = _answer;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
 // Do any additional setup after loading the view.
}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

//なぞなぞの回答を設定する共通メソッド
- (void) riddleAnswer:(NSString*)answer {
    //回答を設定
    _answer = answer;
    //回答のViewControllerへ遷移するセグエのIdentifierを設定し、
    //コード上から遷移させる。
    [self performSegueWithIdentifier:@"AnswerSegue" sender:self];
}

//このメソッドはセグエが実行されて画面が遷移する前に呼び出されるメソッド
//オーバーライドして、処理を追加。
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"AnswerSegue"]) {
        //遷移先のViewControllerインスタンスを取得
        AnswerViewController *avc = segue.destinationViewController;
        avc.answerMessage = _answer;
    }
}

//ぞう
- (IBAction)elephant:(id)sender {
    [self riddleAnswer:@"ざんねん"];
}
//くま
- (IBAction)bear:(id)sender {
    [self riddleAnswer:@"ざんねん"];
}
//しか
- (IBAction)deer:(id)sender {
    [self riddleAnswer:@"正解!"];
}

@end
各Actionから呼び出されるメソッドではriddleAnswerというメソッドを呼び出している。引数から分かる通り 回答結果の文字列を設定している。なぞなぞの正解は「しか」なのでしかのボタンActionでは「正解!」という文字列が設定される。 riddleAnswerのメソッドでは、引数の文字列を_answerというインスタンス変数に代入している。そして、コード上から セグエの遷移実行を行う。
[self performSegueWithIdentifier:@"AnswerSegue" sender:self];
performSegueWithIdentifierの第一引数はSegueのIdentifierを指定する。 これで、ボタンが押されたら回答の画面に遷移する。

次に、回答の画面(AnswerViewController)へ回答結果を受け渡す必要がある。 Segueが実行される前に呼び出されるメソッドがあり、そのメソッドをオーバーライドすることでSegueが実行される 前に遷移先のインスタンスへデータを設定する。
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender;
まず、Segueが自分が意図している遷移かを判定する。判定はSegueのIdentifierを使用して判定する。 本当に自分の意図したSegueだった場合、遷移先のViewControllerインスタンスを取得し、インスタンスへデータを設定する。遷移先のインスタンスは 引数で受け渡されるsegueからsegue.destinationViewController;で取得出来る。 受け取り側ViewControllerであるAnswerViewController.mでは受け取った文字列を- (void)viewDidLoad メソッド の中でラベルのTextに設定する。
#import "AnswerViewController.h"

@interface AnswerViewController ()

@end

@implementation AnswerViewController
@synthesize answer = _answer;
@synthesize answerMessage = _answerMessage;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
 // Do any additional setup after loading the view.
    
    _answer.text = _answerMessage;
    
}

- (void)viewDidUnload
{
    [self setAnswer:nil];
    [super viewDidUnload];
    // Release any retained subviews of the main view.
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

@end
以上で実装は終わり。見て分かる通りこれでもソースコードの実装はたいして行なっていない。
個人的にはコード量が云々というより、やはり画面遷移がビジュアル的に表現されているのが素晴らしいと思う。
Storyboardによる「戻る」実装についてはStoryboardでNavigationControllerの戻るを実装する(UnWindによる実装)をご参照下さい。

3 件のコメント :

  1. わかりやすい説明ありがとうございました。segueについてやっと分かったような気がしました。

    返信削除
  2. はじめまして,iOS開発初心者です。随分前のエントリですが今も反応があることを期待しつつ,
    NavigationControllerの挙動についてよくわからない部分があるのでご助言いただきたくコメントしました。

    「NavigationController以下のページが全て閉じられた時,戻って表示されるページがNavigationControllerより1つ前のページになってしまいます。」

    NavigationController - Master 以下に Detail と Add ページがあり,Addページを閉じたときにMasterに戻って欲しいのですが,Masterページが一瞬見えたあと,NavigationController以前のページが表示されます。

    この現象はなぜ起こるのでしょうか?

    返信削除
    返信
    1. はじめまして。本ブログを参考頂いて有難うございます。

      ご質問の件ですが、「Masterページが一瞬見えたあと,NavigationController以前のページが表示されます。」との事なので、MasterのViewController内の画面初期化系処理でNavigationController以前の画面へ戻る処理がコード上で実装されている事はないでしょうか。
      画面初期化系の処理とはviewDidLoad()とかviewDidAppear(_ animated: Bool)等の画面が表示される前に呼び出されるメソッドです。
      記載された情報からはこの程度の助言になってしまいますが、もう一度ソースコードを確認してみてください。色々と難しいこともあるとおもいますが、頑張ってくださいね。
      ちなみに、私はそのような現象にあったことが無いです。
      それでは。

      削除

'},ClipboardSwf:null,Version:'1.5.1'}};dp.SyntaxHighlighter=dp.sh;dp.sh.Toolbar.Commands={ExpandSource:{label:'+ expand source',check:function(highlighter){return highlighter.collapse;},func:function(sender,highlighter) {sender.parentNode.removeChild(sender);highlighter.div.className=highlighter.div.className.replace('collapsed','');}},ViewSource:{label:'view plain',func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/'+code+'