Download - さらに上を目指すための iOS アプリ設計
さらに上を目指すための
iOS アプリ設計
@taketo1024
(このスライドはヤフー社内で「中級者iOSアプリ開発者」向けに行った講義の資料です)
本講座の目的• iOSアプリ開発者がより高い視点からアプリの設計を考えられるようになること。
• デザインパターンなどのお堅い話ではなく、「いい設計」を感覚的に納得できるようにしたい。
• 「いい設計」を意識できるようになり、プロダクトの品質と開発スピードが上がれば嬉しいです。
Agenda1. アプリ開発における「いい設計」とは?
2. Storyboard / xib / コードの住み分け
3. delegate / callback / notification の使い分け
4. インターフェース ~ 晒すものと隠すもの
5. 肥大化したクラスをスリム化する
6. 通信処理のカプセル化と使い捨て
7. 外部ライブラリの使用を判断するポイント
8. おまけ:これからの Swift 対応のために
1. アプリ開発における「いい設計」とは?
やってはいけないこと
最初から「神殿のような設計」を求めない方がいい。
アプリ開発において受け入れるべき 「3つの変化」
1) OS/デバイス/開発言語/フレームワークの進化
• 最新のものでも1年後には当たり前のように陳腐化している。
• ARC がなかった時代、block がなかった時代とでは「最適な設計」は当然違う。
2) ユーザニーズ/トレンドの変化
•「最適な設計」が完成する頃にはもうそれ自体不要になっていたりする。
• アプリ全体に影響を与えるようなフレームワークの採用には注意(例:Three20)。
3) チーム/メンバーの変化
• 人材流動化の時代。いつでもメンバーは入ったり抜けたりする。
• サービスやチームの規模も大きくなれば、設計のあるべき形も当然変わる。
変化に対応できる「いい設計」の要件
1) 柔軟性
• プロダクトの要件や仕様の変更にすぐ対応できる。
• 特定のライブラリやフレームワークにできるだけ依存しない。
2) 拡張可能性
• 一極集中せず、機能を並列的に付け足していける。
• 逆に不要になったものは簡単に取り外せる。
3) 安定性
• 何かをちょっと変えただけで落ちまくるようになっては困る。
参考:「メタボリズム」
建築には空間的な制約があるが、ソフトウェアは真にメタボリックな設計が実現可能。
メタボリズムは、1959年に黒川紀章や菊竹清訓ら日本の若手建築家・都市計画家グループが開始した建築運動。新陳代謝(メタボリズム)からグループの名をとり、社会の変化や人口の成長に合わせて有機的に成長する都市や建築を提案した。
彼らの構想した将来の都市は、高度経済成長という当時の日本の人口増加圧力と都市の急速な更新、膨張に応えるものであった。
彼らは、従来の固定した形態や機能を支える「機械の原理」はもはや有効的でないと考え、空間や機能が変化する「生命の原理」が将来の社会や文化を支えると信じた。 …
引用:Wikipedia 中銀カプセルタワービル黒川紀章
僕はこう思うッス
慣れない内はまずはスピード優先で作っていい。開発速度
が鈍ってきたらリファクタリングを検討しよう。そのとき
にちゃんと時間を取らせてもらえるようにマネージャとの
信頼関係を築いておくことも大事です。
2. Storyboard / xib / コード の住み分け
あかんパターンその1. 激重スパゲッティストーリーボード
• 画面数が多くなると重く、可視性も悪くなる。
• チーム作業だとコンフリクトしまくってやばい。
あかんパターンその2. 全部コードで書いてる
• UI の微調整が大変になる。
• チーム作業の場合つらい。デザイナとの分業もできない。
- (void)viewDidLoad { [super viewDidLoad]; UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(10, 200, 300, 40)]; textField.borderStyle = UITextBorderStyleRoundedRect; textField.font = [UIFont systemFontOfSize:15]; textField.placeholder = @"Message"; textField.autocorrectionType = UITextAutocorrectionTypeNo; textField.keyboardType = UIKeyboardTypeDefault; textField.returnKeyType = UIReturnKeyDone; textField.clearButtonMode = UITextFieldViewModeWhileEditing; textField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter; textField.delegate = self; [self.view addSubview:textField]; … }
なかなか最適解がない
• Storyboard のいいところ
• アプリの画面構成・遷移が一望できる。
• 簡単な遷移ならコードを書かなくてもいい。
• UI をグラフィカルに構成していける(デザイナと分業できる)。
• もどかしいところ
• 画面遷移も構成も一個のファイルに詰め込んだら当然重くなる。
• 文字列の ID でコードと紐付けなきゃいけない。
• 遷移間の処理は prepareForSegue:sender: で分岐しなきゃいけない。
僕のオススメのやり方
• Storyboard は画面遷移を定義するためだけに使う。
• 各コントローラの UI 構成は xib で作る。
• 画面の遷移処理は performSegueWithID: でやる。
Storyboard で画面遷移、xib で画面構成
Viewを外しておく
+
対応するファイル名の xib が自動で読み込まれる!
これで全体の画面遷移と各々の画面構成が分離できる
+
…
Storyboard で画面遷移、xib で画面構成
Storyboard 非対応な外部ライブラリもこの方法で行ける
#import "XYZStoryboardSegue.h"
@implementation XYZStoryboardSegue
- (void)perform { // 具体的な処理 }
@end
独自 UIStoryboardSegue を作れば、どんな遷移も繋げる
performSegueWithId: で遷移処理
- (void)cellTapped:(XYZTableViewCell *)cell { XYZWebEntity *entity = cell.entity;
[self performSegueWithIdentifier:@"Browser" preparation:^(UIStoryboardSegue *segue) {
XYZWebViewController *vc = segue.destinationViewController; vc.title = entity.title; vc.URL = entity.linkURL; }]; }
• prepareForSegue: に遷移処理をまとめて分岐させるのは嫌なので、遷移時にブロックを渡せるように拡張した(method-swizzling によるちょい裏技)。
• StoryboardID を別ファイルにしたり、遷移処理をメソッド化してカテゴリと切り出したりすれば、各 ViewController では Storyboard のこと意識せずに済む。
• このやり方の難点
• xib だと navigationItem を指定したりできない(Outlet で繋いでおいてコードから指定することはできる)
• 画面遷移処理は全てコードで記述しなきゃいけない。
• その他のやり方
• xib を使わず 画面遷移用の Storyboard と各画面別の Storyboard を分ける。
• Segue は使わず、 instantiateViewControllerWithIdentifier: によってコードで VC を生成して画面遷移。
僕はこう思うッス
現状最適なソリューションはないので、画面の数やチーム人数に応
じて上手くやってける方法を探っていきましょう。
3. delegate / callback / notification の 使い分け
原則:相互参照は良くない
• 特殊なケースを除いて、オブジェクトが相互に参照しあってメッセージを送り合うのは密結合になってよくない。
• アプリは基本的に木構造になっているので、子が親を参照したり、子同士で参照しあったりしないように気をつけたい。
• オブジェクト間の依存関係を必要最小限に制限しながら通信する方法としてある delegate / callback / notification の3パターンを解説します。
親
子
親
子 子
××
A) delegate パターン
• UITextFieldDelegate, UITableViewDelegate, UIImagePickerControllerDelegate など。
• 子(委譲する側 = delegater)は親(委譲される側 = delegate)の詳細については知らず、必要に応じて問い合わせ/丸投げする。
• TableViewController(親)が TableView(子)を持つような、親が子を管理していてその存在期間中に密なやり取りがある場合に有効。
• 1対N には不向き(親のメソッド内で分岐が必要になる)
• VC 間の通信も delegate パターンになっていることが多い。
親 (delegate)
子 (delegater)
子は無責任に問い続ける
B) callback パターン
• UIAlertControllerAction, …WithCompletionHandler: など。
• 親が子に対してやっておいて欲しい処理を予め指定しておく。
• 子への命令とコールバックをまとめて書けるのが利点。
• 通信などの単発の命令の完了処理や、処理中に密なやり取りをする場合に有効。
• AlertView / ActionSheet が callback パターンになったのは必然。
親
子
子
用が済んだらサイナラ
C) notification パターン
• UIApplicationWillEnterForegroundNotification, UIKeyboardWillShowNotification など。
• 通知者が受信者が誰なのかを直接知らない場合に有効。
• 受信者からのレスポンスを受け取りたい場合は使えない。
• ある画面でコンテンツを Like したのが、他画面でも反映されてて欲しい場合などに有効。
• コード上では依存関係が見えにくいので、仕様変更による不具合には注意が必要。
NotificationCenter
親1 親2 親3
子
(親子という表現は適切でないが、前の二つとの関連のため)
どのパターンにせよ、 「子は親のことを知らなくていい」ことが大事!
親
子
delegate
親
callback
子
子
notification
NotificationCenter
親1 親2 親3
子
僕はこう思うッス
いちいちプロトコルを作るのは面倒だから直接参照したくなるけど、
放っておくとすぐスパゲッティ化するので、早いうちから不必要な
依存性はシャキッと断ち切っておきましょう。
4. インターフェース ~ 晒すものと隠すもの
公開プロパティ/メソッドは できるだけ少なくシンプルにしておく。
• クラスの内部状態/内部でのみ使う操作は private にしておきましょう(Obj-C なら無名カテゴリ/Swift なら private で)。
• 外から変更されることのない property は readonly / immutable に。
• サブクラス化前提のクラスで、子クラスのみに公開する必要のあるものも public にしない。(Swift なら internal で、Obj-C はサブクラス専用ヘッダを用意する)
• 「このクラスの役割は何なのか」を意識し、「適切な名前がつけられるかどうか」を正しい設計の指針にする。
僕はこう思うッス
細かなコーディング規約よりもインターフェースの正しさを意識する
方がずっと大事。
クラスの内部実装が汚いのは直しようがあるが、インターフェースが
イビツだと修正が大変(内装のリフォーム < 骨格の改築)。
5. 肥大化したクラスをスリム化する
BaseViewController の危険性
• 共通処理をなんでもかんでも BaseVC に入れると、どんどん Fat 化してどこに何があるのか分からなくなる。メンテナンス性も下がるし、変更の影響が見えづらく危険。
• 共通処理のうち一部カスタマイズできようにしようとする必要が出てきたりして、サブクラスで共有のパラメータを用意したりテンプレートメソッドを作るハメになって余計ややこしくなる。
• iOS がアップデートして BaseVC でやってる処理が不要になっても容易に取り外せなかったりする。
スリム化の戦略をいくつか
A. モジュール化して切り出し
B. 基底クラスをカテゴリ拡張
C. クラスの実装を分割
A. モジュール化して切り出し
• 例えば TableVC / CollectionVC の delegate / dataSource を別クラスにする。
• 切り出したモジュールとの間での相互参照が増えすぎないように気をつけないといけない(意外とこれが難しいので何でも切り出せばいいというものでもない)。
• 切り出したモジュールが専用の protocol を用意して、親への参照をなくしたりする。
TableViewController
TableViewDelegate
TableViewDataSource
TableViewDelegate
TableViewDataSource
TableViewController
B. 基底クラスをカテゴリ拡張
• 共通処理のうち、独立性の高い機能を切り出して UIViewController 拡張にする。
• ロギング、キーボード表示関連、アラート/HUD表示 など全画面共通の機能。
• これも UIViewController+Common などとするとすぐ Fat 化するので注意。
MyViewController
共通処理 1
共通処理 2
UIViewController + ...
共通処理 1
UIViewController + ...
共通処理 2
C. クラスの実装を分割
• Extension は既存クラスを拡張するためだけでなく、自分で作ったクラスの実装を分割するためにも使える。
• 例えば AppDelegate にはいろんな機能が詰め込まれがちだけど、互いに無関係なものが多いので、機能群をまとめて分割するといい。
MyClass
まとまり1
まとまり2
MyClass
MyClass + まとまり1
MyClass + まとまり2
僕はこう思うッス
最初から何でもかんでも共通化/クラス分割しようとすると危険。
特にクラス階層を深くするのは慎重に。全体像が見えてないうちは
ベタ書きのまま我慢。
美意識よりも効果を優先しましょう。
6. 通信処理のカプセル化と使い捨て
通信処理のカプセル化
• Controller / Model に生の通信処理を書くべきではない。
• endpoint-URL や、リクエストパラメータ、レスポンスの仕様について利用者が意識しなくて済むようにしたい。
• レスポンスは Dictionary などのプリミティブ型ではなく、専用のエンティティクラスを用意した方がいい。型セーフになるし API の仕様変更に対しても柔軟に対応できる。
僕が好きなやり方: 通信自体をオブジェクトとして使い捨てる
- (IBAction)searchButtonTapped:(UIButton *)button { NSString *query = …; XYZSearchRequest *req = [XYZSearchRequest requestWithQuery:_query];
[req startWithHandler:^(XYZSearchResultSet *result, NSError *error) { // 通信コールバック if(error) { … // エラー処理 return; } … }]; }
• 通信開始のための手続きがシンプルでいい。 • インスタンス保持しておけば通信をキャンセルすることもできる。
僕はこう思うッス
AFNetworking のようなガッツリした通信ライブラリは本当に必要
か見直してみましょう。 API 仕様の特殊性を吸収する簡単なラッパー
があればほとんどの場合十分。
7. 外部ライブラリの使用を判断するポイント
• まずソースコードは必ず確認する。
• IF仕様がイケてなかったら実装もきっとイケてない(クラッシュの原因にもなりうる)。
• コードが公開されていないものは信頼性が保証されている場合に限定すべき。
• 他にも同様のライブラリがないか確認する。★数や最終コミット日時(継続的にメンテナンスされてるか)なども確認する。
• 分厚いライブラリの場合は警戒する。全面的にその仕様に依存した設計になってしまうのはリスク(例: Three20)。
• 自分が欲しい機能はそのライブラリの何割を占めるか?一部だけならそのコードにインスピレーションを受けた上で自作した方がいいかもしれない。
• それでも使用した方がよければ、ライセンスを確認した上で使わせて頂きましょう。
8. (おまけ)これからの Swift 対応のために
Swift 対応
既存コードを機械的に書き換える Swift 最適化+
こっちの作業は機械がやればいい!
objc2swift project
https://github.com/yahoojapan/objc2swift
Fork me!
総まとめ:僕はこう思うッス
よくできた設計は Storyboard とクラスのインターフェースを一望
するだけで大体の構造が分かる。細かくルールを定めるよりも先に、
どういう設計を目指すのかチームのみんなで合意しておきましょう。
Questions?
Thanks!
@taketo1024