24/7 twenty-four seven

iOS/OS X application programing topics.

Storyboardを1画面ごとに分割した話

今年の5月くらいの話なのですが、ユビレジのiPadアプリケーションのプロジェクトで使っているStoryboardを基本的に1画面(≒1 View Controller)の単位に分割するということをしました。


1画面1Storyboardメソッドについてはnakiwoさんが書かれた記事も参考になります。

1画面から始めるStoryboard - Cocoaメモ


↑ 上記の資料はどちらかというとStoryboardを使い始めるにあたって、1画面単位で少しずつ使っていこうという感じですが、ユビレジではもともとほぼ全部の画面がStoryboardになっていました。

ただ複数人で共同作業をするにあたっては、1画面単位を1ファイルにしておくくらいがメンテナンスしやすいんじゃないかなあという結論になったのでしばらくそういうふうに運用することにしました。


また、XIBと違ってStoryboardは単純にコピー&ペーストで別のファイルにまったく同じ構成のものを移すことができるので試すのも戻すのも非常に簡単なので合わなかったら戻せばよいと考えました。
(XIBだと単純なコピー&ペーストだとアクションの関連が外れたりします。まあXIBに複数のコンポーネントがあってそれを分けたいということは滅多にないのでそれは特に問題ではないですが)


複数のView Controllerを含む大きなStoryboardに比べて、小さなStoryboardのメリットしては、

  • コンフリクトの可能性が減る(大きなStoryboardだと、全く別の画面の修正にもかかわらず同じファイルになるので)
  • Auto LayoutのOn/Offを画面ごとに柔軟に設定できる(Auto Layoutの設定はファイルごとなので)
  • XIBと同様に使える(StoryboardはXIBの上位互換)

があります。

あと大きなStoryboardは作った当人は慣れているので、どこに何があるかすぐ分かるけど、他のひとにとっては目的の画面を探すのもけっこう大変だったりします。


特にユビレジでは4/5月にiOSのエンジニアが増えたので、Storyboardのマージに気を使うことが多くなることを避けたいというのが第一の大きな理由で、次に新しい人が既存のコードを追っていくなかでStoryboardを見るときに迷わないようにしたいという点です。

デメリットとしては、基本的に1つのStoryboardファイルには1つの画面しかないので、画面の遷移にSegueを利用することはできなくなります。

ただその点については、Segueを使うこと自体がそれほどメリットではないということで、トータルで考えるとそれほどデメリットにはならないという判断をしました。


Segueを使う場合であまり良くないと考えていることの1つは、多くのケースでは画面を遷移するときにいくつかデータを受け渡す必要があり、Segueを使う場合は標準のAPIではその部分がどうしても微妙になってしまうという点です。


例えば典型的なSegueを使った画面の遷移は下記のようになります。

- (IBAction)nextButtonTapped:(id)sender
{
    [self performSegueWithIdentifier:NSStringFromClass([UBCheckoutViewController class]) sender:self];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:NSStringFromClass([UBCheckoutViewController class])]) {
        UBCheckoutViewController *controller = segue.destinationViewController;
        controller.delegate = self;
        controller.checkout = self.checkout;
    }
}

↑ このように、ユーザーのアクションを受けて次の画面に必要なパラメータを渡して遷移するというごく普通のケースを記述するのに、画面を遷移するという処理と、別の処理で遷移先を判断してパラメータをセットするという2つに少なくとも分かれてしまいます。
さらに、Storyboard上で設定するSegueのIDがコードに文字列として(!)現れてしまうというのも残念なところです。


いちおう、SegueのIDは遷移先のビューコントローラの名前と同じにするというルールにして、コード上ではクラス名から取得するという形で、リファクタリングなどがある程度有効になるようにしていますが、Storyboard上でIDを修正し忘れたり、両方を一緒に修正しなければならないというのは結局変わらないので、やっぱりイマイチだなと思います。


1画面1Storyboardの場合は、従来の遷移先のビューコントローラを作ってデータをセットして遷移という形式で書くことができます。

- (IBAction)nextButtonClicked:(id)sender
{
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Checkout" bundle:nil];
    UBCheckoutViewController *controller = [storyboard instantiateInitialViewController];
    controller.delegate = self;
    controller.checkout = self.checkout;
    
    [self presentViewController:controller animated:YES completion:NULL];
}


↑ 上記の場合でもUBCheckoutViewControllerをインスタンス化するには「Checkout」という名前のStoryboardを使うという遷移元のビューコントローラが特に知る必要のない情報が書かれてしまうのが残念なので、少し工夫して下記のように、Storyboardからインスタンス化する部分をそのビューコントローラのクラスメソッドに隠蔽しています。
(ちなみにStoryboardのファイル名もビューコントローラ名にすればいいかと悩みましたが、厳密に1画面だけにするべき、とはしたくなかったのでそれはやめました)

- (IBAction)nextButtonClicked:(id)sender
{
    UBCheckoutViewController *controller = [UBCheckoutViewController checkoutViewController];
    controller.delegate = self;
    controller.checkout = self.checkout;
    
    [self presentViewController:controller animated:YES completion:NULL];
}


初期データを設定するだけのためにプロパティを公開するのは嫌だ、という場合にはイニシャライザで渡すように書くのも簡単です。

- (IBAction)nextButtonClicked:(id)sender
{
    UBCheckoutViewController *controller = [UBCheckoutViewController checkoutViewControllerWithCheckout:self.checkout delegate:self];
    
    [self presentViewController:controller animated:YES completion:NULL];
}


こうしておけば、UBCheckoutViewControllerの作成方法は使う側には関係がないので、仮にStoryboardを使わずにコードで生成するように変わったとしても使う側に影響はありません。


他社のプロジェクトで私が見た例だと、Factoryクラスを用意してそれを通してView Controllerを取得するように作っているところもよく見ます。
もちろんケースバイケースですが、1クッション挟むわりには特に抽象度も結合度もそれほど変わらないと思ったので、それなら直接相手のView Controllerを使うほうがわかりやすいと考えて上記の方法を採用しています。


余談ですが、下記の所さんの記事など、ヘルパーライブラリを作って解決する方法もあります。
ただ、我々は画面の遷移という基本的な処理を理解するのに標準でないAPIをまず知らなければならないという状況は、少なくとも人の変化がこれからも頻繁に起こることが考えられる中では避けたいと判断しました。

Storyboardでの画面遷移をスマートにやる方法 - TOKOROM BLOG


また、少なくとも私は上記の記事にある遷移元に遷移先の情報が必要になる(依存関係がある)という状況は特に問題とは考えていないので(逆はかなり問題があります)先に書いたくらいがバランスいいのではと思っています。


別にSegueはダメな子なので使うなというわけではもちろんありません。
新しいものをイチから作っていくときなど、試行錯誤が必要なときで一人で作るぶんには1つのStoryboardでSegueを使っていくのはとても早く作れるので非常に便利ですし、私も新しいものはガンガンSegueで繋いで作っています。


ただ、ある程度UIが落ち着いてきて、細かい改修だったり共同作業が増えてきた段階になると、
細かい単位にStoryboardを分けることで変更がやりやすくなるかどうかを検討してもいいんじゃないかという話です。


参考までに分割前と、後のスクリーンショットを載せておきます。


分割前:
(Storyboardはこれを含めて3つあった中でメインに使っていたものです)
20131117181446


分割後:
(基本的には1画面1Storyboardですが、一部の設定画面やモーダルビューは関連する画面をまとめたまま残しています)

20131117181446

20131117181446

20131117181446

20131117181446

20131117181446