InterfaceBuilderとViewControllerを使用したシーン管理の両立について

ちょうど2週間ほど前から職業iOSプログラマーとして働かせてもらって、色々思い出すところから始めたりで大変でしたが、なんとか今日一つの形になるものが出来上がった感じです。
ここらで題名にあるXcode4.2におけるInterfaceBuilderとシーン管理の両立について書こうと思います。

・なぜシーン管理が必要なのか

画面A → 画面B → 画面C

上記のように遷移する場合、Viewを切り替えていくのは一つの方法としてありだと思いますが、Apple公式のガイドやNavigationController、TabBarControllerの設計思想を見るにひとつの画面をViewControllerで定義するのが最も自然なやりかになるかと思います。

ViewControllerA  → ViewControllerB → ViewControllerC

こうなりますね。それでは実際の遷移はどのようにすれば良いでしょうか。
通常、空のプロジェクトを作成するとAppDelegateがUIWindow windowを持っておりこれが全てのrootになっていますので、この切り替えはViewControllerA〜C自身で行うレベルではない。という事が分かると思います。

そこで私はViewControllerA〜CにAppDelegateのdelegateを持たせる事にしました。delegateとはC言語でいうオブジェクトポインタで、ViewControllerA〜CからAppDelegateのメソッドを呼び出す事ができます。

ViewControllerA「ViewControllerBに移動したいよAppDelegateさん」
ViewControllerB「ViewControllerCに移動したいよAppDelegateさん」

シーン管理をしない場合、AppDelegateには上記の台詞分のメソッドが必要になります。

AppDelegate「ViewControllerAからViewControllerBに移動するためのメソッドを準備。」
AppDelegate「ViewControllerBからViewControllerCに移動するためのメソッドを準備。」

画面が増えれば増えるほどただ画面遷移させるだけのメソッドがどんどん量産されていく事になりますし、やっぱりViewControllerBをViewControllerDにしようといったときにもこの手間が発生してしまいます。
protocolの定義、クラスの定義、メソッドの本体の3箇所を編集するのは正直面倒くさいですよね。



・シーン管理を使おう

そこでNavigationControllerを使用した上で、画面遷移時のメソッドをまとめて管理することを考えます。
NavigationControllerの使い方についてはAppDelegate自体にNavigationControllerを持たせて

[window addSubview:[navigationController view] ];

とした上でNavigationControllerに付属している

pushViewController:animated:
popViewControllerAnimated:
popToRootViewControllerAnimated:
popToViewController:animated:

を使ってViewController自体を入れたり出したりします。
肝心なシーン(ViewController)の管理については

NSArray sceneArray = [NSarray arrayWithObjects:ViewControllerA, ViewControllerB, ViewControllerC, nil];

とまずひとつの配列に全てのViewControllerを格納してしまった上で、

#define APPSCENE_A 0
#define APPSCENE_B 1
#define APPSCENE_B 2

@protocol AppDelegate
(void)pushScene;
(void)popScene;
@end

と定義し、

[AppDelegate pushScene:APPSCENE_A];
[AppDelegate popScene];

という感じで呼び出せるように実装します。
これで一つのメソッドでさまざまなシーンを呼び出せるようになりましたね。



・シーン管理の拡張

上記のようなシーン管理をすることで、ViewControllerの切り替わり時に必要な処理も定義してしまうことができます。
例えば次のようなprotocolを用意してみます。

@protocol AppScene
(void)initScene;
(void)endScene;
(void)sleepScene;
(void)resotreScene;
@end

これを

ViewControllerA : UIViewController
ViewControllerB : UIViewController
ViewControllerC : UIViewController

という風に管理する全てのViewControllerに適用してあげることで、pushSceneやpopSceneの中から遷移時に必要な処理を実行させることができます。
ただしinitSceneについてはViewControllerのViewDidAppearで十分ということもありますし、sleepSceneやresotreSceneを使う機会があまりない場合には、ViewControllerBの中でViewControllerCを呼び出したフラグ等を用意して管理するのが楽かもしれません。(もともとViewControllerBの反応がトリガーになりますしね。)
またrestoreSceneなんかはNavigationContrller自体のViewControllerのスタック管理と並行して、AppDelegate側でもシーンをスタック管理することが必要になったりします。



・フェードインとフェードアウト

最後にちょうど今日会社で実装してみたフェードインとフェードアウトについて考えます。
シーン管理の方式をとるとViewController同士の接続が途中で分断されているため、フェードインとフェードアウトの実装が雑多な状況になるかと思います。

ViewControllerA:「よし、フェードアウトして真っ白になったらpushSceneするぞ」
ViewControllerB:「Aがフェードから来るから俺はフェードインするぞ」

これの一番の問題点はやはり拡張性が非常に低いということで、やっぱり黒フェードに変更しようとか、画面の接続を変えようという場合に絶望します。私自身もソースが本当に汚い状態になっていましたので、次のようにwindowにフェード用のUIViewを追加でaddSubviewをする仕組みを考えました。

[window addSubview:[navigationController view] ];
[window addSubview:fadeManagerView];

上記の後、fadeManagerViewに白マット画像や黒マット画像を読み込み、アルファ値を調整してフェードを実現します。
ここで次の新しいpush, popのprotocolを定義すると今までのシーン管理の苦労が報われることになります。

@protocol AppDelegate
(void) pushScene:(NSUInteger)sceneID
 FadeType:(FMFadeType)
 duration:(NSTimeInterval)duration
 selector:(SEL)selector;
@end

実際の実装自体はクラスを分けたりしていますが、上記メソッドでフェードアニメーションを開始、中央値(画面が一色)のときにpushScene、フェードアニメーション終了時点で遷移先のメソッドを呼び出すという形で実装することができ、コード全体の2割ほどが不要となりました。
※遷移先のメソッドを呼び出すには先述のNavigationController内のスタック管理と並行した独自のスタック管理が必要です。


・まとめ

web上ではViewControllerの管理に関する文献も少なかったので、今回はちょっと独自の内容を書いてみました。
シーン管理をすることで最終的には色々な手間を省く事ができますし、根本はコピペで使い回すことができるので生産性もとても良いと思います。私自身も2週間とまだまだ経験が浅いですが何か問題やひらめいたことがあれば、このブログで追記していければと思います。