建築の分野では、私たちが建物を形作り、その後、建物が私たちを形作るということはよく知られていることです。 すべてのプログラマーがいずれ学ぶように、これはソフトウェアの構築にも同様に当てはまります。
各ピースが容易に識別でき、特定の明白な目的を持ち、論理的な方法で他のピースと一緒にフィットするように、コードを設計することが重要です。 これは、ソフトウェア アーキテクチャと呼ばれるものです。 優れたアーキテクチャは製品を成功させるものではありませんが、製品を保守しやすくし、保守する人の正気を保つのに役立ちます!
この記事では、VIPER と呼ばれる iOS アプリケーション アーキテクチャへのアプローチを紹介します。 VIPER は多くの大規模なプロジェクトを構築するために使用されていますが、この記事の目的では、ToDo リスト アプリを構築することによって VIPER をお見せします。 GitHub:
VIPERとは
テストは、iOSアプリの構築において必ずしも主要な部分ではありません。 Mutual Mobile でテスト手法を改善するための探求に着手したとき、iOS アプリケーションのテストを書くのは難しいということがわかりました。 ソフトウェアのテスト方法を改善するつもりなら、まず、アプリケーションを設計するためのより良い方法を考え出す必要があると判断しました。 私たちはその方法を VIPER と呼んでいます。
VIPER は Clean Architecture を iOS アプリケーションに応用したものです。 VIPER という単語は、View、Interactor、Presenter、Entity、および Routing の頭文字をとったものです。 Clean Architecture は、アプリケーションの論理構造を責任の明確な層に分割します。 これにより、依存関係 (たとえば、データベース) を分離し、層間の境界で相互作用をテストすることが容易になります。
VIPER は View Interactor Presenter Entity Routing の略です。 アプリケーション アーキテクチャとして MVC を使用すると、すべてのクラスがモデル、ビュー、またはコントローラーのいずれかであると考えるよう導かれます。 アプリケーションロジックの多くはモデルやビューに属さないため、通常はコントローラに置かれることになります。 このため、ビューコントローラが過剰な処理を行うことになり、 Massive View Controller と呼ばれる問題が発生します。 これらの巨大なビュー コントローラーをスリム化することは、コードの品質を向上させようとする iOS 開発者が直面する唯一の課題ではありませんが、それは素晴らしい出発点です。
VIPER の明確なレイヤーにより、アプリケーション ロジックおよびナビゲーション関連のコードに明確な場所を提供することで、この課題に対処することができます。 VIPER を適用すると、To-Do リストの例のビュー コントローラーが、無駄のない、平均的な、ビュー制御マシーンであることに気づくでしょう。 また、ビュー コントローラーおよび他のすべてのクラスのコードが理解しやすく、テストしやすく、その結果、保守も簡単であることがわかります。
Application Design Based on Use Cases
アプリケーションはしばしば一連の使用事例として実装されます。 ユースケースは受け入れ基準、または動作としても知られており、アプリが何をするものであるかを記述します。 たとえば、リストは日付、タイプ、または名前によって並べ替えられる必要があります。 これがユースケースです。 ユースケースは、ビジネスロジックを担当するアプリケーションの層です。 ユースケースは、それに対するユーザーインターフェースの実装から独立していなければなりません。 また、小さく、よく定義されている必要があります。 複雑なアプリをより小さなユースケースに分解する方法を決定することは困難であり、練習が必要ですが、解決しようとしている各問題と書いている各クラスの範囲を限定するのに役立つ方法です。
Viper でアプリを構築するには、各ユースケースを満たすために一連のコンポーネントを実装する必要があります。 アプリケーション ロジックは、ユース ケースを実装する主要な部分ですが、それだけではありません。 ユースケースはユーザーインターフェイスにも影響します。 さらに、ユースケースは、ネットワークやデータ永続化など、アプリケーションの他のコアコンポーネントとどのように組み合わされるかを考慮することが重要です。 コンポーネントはユース ケースのプラグインのように機能し、VIPER は、これらのコンポーネントのそれぞれの役割が何であるか、そして、それらが互いにどのように相互作用できるかを記述する方法です。
ToDo リスト アプリのユース ケースまたは要件の 1 つは、ユーザーの選択に基づいて ToDo を異なる方法でグループ化することでした。 そのデータを整理するロジックをユースケースに分離することにより、ユーザー インターフェイス コードをクリーンに保ち、ユースケースが期待どおりに動作し続けることを確認するために、簡単にテストでラップすることができるようになりました。
VIPER の主要部分
VIPER の主要部分は次のとおりです。
- ビュー: Presenter から指示されたものを表示し、ユーザー入力を Presenter に戻します。
- インタラクター:使用事例によって指定されるビジネス ロジックが含まれています。
- Presenter: (Interactorから受け取った)表示用のコンテンツを準備し、(Interactorに新しいデータを要求して)ユーザー入力に反応するためのビューロジックが含まれます。
- Entity: Interactorが使用する基本モデルオブジェクトが含まれます。
- Routing: どの画面をどの順序で表示するかを記述するためのナビゲーションロジックが含まれます。
この分離も単一責任原則に適合しています。 Interactor はビジネス アナリストに、Presenter はインタラクション デザイナーに、View はビジュアル デザイナーに責任を持ちます。
以下は、異なるコンポーネントとそれらの接続方法についての図です。
VIPER のコンポーネントは、アプリケーションにどのような順序でも実装できますが、ここでは、実装を推奨する順序でコンポーネントを紹介することにしました。 この順序は、製品が何をする必要があるかを議論することから始まり、次にユーザーがどのように操作するかを議論する、アプリケーション全体を構築するプロセスとほぼ一致していることに気づかれることでしょう。 これには、特定のタスクを実行するためにモデル オブジェクト (Entity) を操作するビジネス ロジックが含まれます。 インターアクターで行われる作業は、いかなる UI からも独立している必要があります。 インターアクターは主にロジックを含む PONSO (Plain Old NSObject
) であるため、TDD を使用して簡単に開発できます。
サンプル アプリの主な使用例は、ユーザーに今後の To Do アイテム (つまり、来週末までに期限があるもの) を表示することです。 このユースケースのビジネス ロジックは、今日から来週末までの期限の To Do 項目を検索し、相対的な期限 (今日、明日、今週末、または来週) を割り当てます。
以下は、VTDListInteractor
:
- (void)findUpcomingItems{ __weak typeof(self) welf = self; NSDate* today = ; NSDate* endOfNextWeek = dateForEndOfFollowingWeekWithDate:today]; ]; }];}
Entity
Entity はインターアクターによって操作されるモデル オブジェクトです。 エンティティはインターアクターによってのみ操作される。 Interactor はエンティティをプレゼンテーション層 (つまり Presenter) に渡すことはありません。
エンティティも PONSO になる傾向があります。 Core Data を使用している場合、管理されたオブジェクトはデータ レイヤーの背後に残るようにしたいでしょう。
Here is the Entity for our to-do item:
@interface VTDTodoItem : NSObject@property (nonatomic, strong) NSDate* dueDate;@property (nonatomic, copy) NSString* name;+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;@end
エンティティが単なるデータ構造であっても、驚かないでください。
Presenter
Presenter は PONSO で、主に UI を駆動するためのロジックで構成されています。 ユーザインタフェースをいつ表示するかを決定する。
ユーザーが新しいToDo項目を追加するために+ボタンをタップすると、addNewEntry
が呼び出されます。 このアクションでは、Presenterはワイヤーフレームに新しいアイテムを追加するUIを提示するよう依頼します。
- (void)addNewEntry{ ;}
また、PresenterはInteractorから結果を受け取り、結果をビューに表示するのに有効なフォームに転換します。
- (void)foundUpcomingItems:(NSArray*)upcomingItems{ if ( == 0) { ; } else { ; }}
Entity は Interactor から Presenter に渡されることはありません。 代わりに、動作を持たない単純なデータ構造がインタラクターからプレゼンターに渡されます。 このため、Presenterでは「実際の作業」が一切行われません。 PresenterはViewに表示するためのデータを準備するだけです。
View
Viewは受動的である。 Presenterが表示する内容を待ちますが、Presenterにデータを要求することはありません。 Viewに定義されたメソッド(例:ログイン画面用のLoginView)は、Presenterがより高い抽象度で通信することを可能にし、そのコンテンツで表現され、そのコンテンツがどのように表示されるかは関係ありません。 PresenterはUILabel
やUIButton
などの存在については知らない。 Presenterは自分が保持するコンテンツとそれをいつ表示すべきかを知っているだけである。 コンテンツをどのように表示するかはViewが決定します。
Viewは抽象的なインタフェースであり、Objective-Cでプロトコルが定義されています。 UIViewController
またはそのサブクラスの1つは、Viewプロトコルを実装します。 たとえば、私たちの例の「追加」画面は、次のインターフェイスを持っています:
@protocol VTDAddViewInterface <NSObject>- (void)setEntryName:(NSString *)name;- (void)setEntryDueDate:(NSDate *)date;@end
ビューとビューコントローラは、ユーザーとの対話と入力も処理します。 ビューコントローラが通常大きくなる理由は簡単で、何らかのアクションを実行するために、この入力を処理する最も簡単な場所だからです。 ビューコントローラをスリムにするためには、ユーザがあるアクションを起こしたときに、関係者に知らせる方法を与える必要があります。
私たちの例では、ビュー コントローラーの追加は、次のインターフェイスに準拠するイベント ハンドラ プロパティを持ちます:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate@end
ユーザーがキャンセル ボタンをタップすると、ビュー コントローラーはこのイベント ハンドラに、ユーザーが追加アクションをキャンセルするよう指示したことを伝えます。
ビューとプレゼンターの境界は、ReactiveCocoa の最適な場所でもあります。 この例では、ビューコントローラは、ボタンのアクションを表すシグナルを返すメソッドを提供することもできます。
Routing
1 つの画面から別の画面へのルートは、インタラクション デザイナーが作成したワイヤーフレームで定義されます。 VIPERでは、ルーティングの責任は、プレゼンターとワイヤーフレームという2つのオブジェクトで共有されます。 ワイヤーフレーム・オブジェクトは、UIWindow
、UINavigationController
、UIViewController
などを所有します。 View/ViewControllerを作成し、ウィンドウに設置する役割を担っています。
Presenterはユーザー入力に反応するロジックを含むので、いつ別の画面に移動するか、どの画面に移動するかを知っているのはPresenterである。 一方、ワイヤーフレームはどのようにナビゲートするかを知っています。 そのため、Presenterはワイヤーフレームを利用してナビゲーションを実行します。 共に、ある画面から次の画面への経路を記述します。
ワイヤーフレームは、ナビゲーションの遷移アニメーションを処理するための明白な場所でもあります。 追加ワイヤーフレームのこの例を見てみましょう。
@implementation VTDAddWireframe- (void)presentAddInterfaceFromViewController:(UIViewController *)viewController { VTDAddViewController *addViewController = ; addViewController.eventHandler = self.addPresenter; addViewController.modalPresentationStyle = UIModalPresentationCustom; addViewController.transitioningDelegate = self; ; self.presentedViewController = viewController;}#pragma mark - UIViewControllerTransitioningDelegate Methods- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { return init];}- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return init];}@end
このアプリは、カスタム ビュー コントローラーの遷移を使用して、追加ビュー コントローラーを表示しています。 ワイヤーフレームは遷移の実行を担当するので、add ビューコントローラの遷移デリゲートになり、適切な遷移アニメーションを返すことができます。
Application Components Fitting in with VIPER
iOS アプリケーションのアーキテクチャは、UIKit と Cocoa Touch がアプリを構築する主なツールであるという事実を考慮する必要があります。 アーキテクチャは、アプリケーションのすべてのコンポーネントと平和的に共存する必要がありますが、フレームワークのいくつかの部分がどのように使用され、どこに配置されるかについてのガイドラインを提供する必要もあります。 MVC を置き換える候補として、ビュー コントローラーを多用することを避けると考えるのは簡単でしょう。 しかし、ビュー コントローラはプラットフォームの中心的存在です。方向転換の処理、ユーザーからの入力への応答、ナビゲーション コントローラのようなシステム コンポーネントとの統合、そして iOS 7 では、スクリーン間のカスタマイズ可能な遷移を可能にします。 非常に便利です。
VIPER では、ビュー コントローラーはまさにそれが意図したとおりのことを行います:ビューを制御します。 私たちの ToDo リスト アプリには、リスト画面用と追加画面用の 2 つのビュー コントローラーがあります。 追加ビュー コントローラの実装は、ビューを制御するだけなので、非常に基本的です。
@implementation VTDAddViewController- (void)viewDidAppear:(BOOL)animated { ; UITapGestureRecognizer *gestureRecognizer = initWithTarget:self action:@selector(dismiss)]; ; self.transitioningBackgroundView.userInteractionEnabled = YES;}- (void)dismiss { ;}- (void)setEntryName:(NSString *)name { self.nameTextField.text = name;}- (void)setEntryDueDate:(NSDate *)date { ;}- (IBAction)save:(id)sender { ;}- (IBAction)cancel:(id)sender { ;}#pragma mark - UITextFieldDelegate Methods- (BOOL)textFieldShouldReturn:(UITextField *)textField { ; return YES;}@end
アプリは通常、ネットワークに接続されているとより魅力的になります。 しかし、このネットワーク接続はどこで行われるべきで、それを開始する責任を負うべきは何でしょうか。 ネットワーク操作を開始するのは通常インタラクタの役目ですが、インタラクタはネットワーク コードを直接処理することはありません。 ネットワーク・マネージャーやAPIクライアントのような依存関係に問い合わせることになる。 インタラクターは、ユースケースを満たすために必要な情報を提供するために、複数のソースからデータを収集する必要があるかもしれません。 その後、Interactor によって返されたデータを取得し、プレゼンテーション用にフォーマットするのは Presenter の役割です。 Interactor がビジネス ロジックを適用すると、データストアからエンティティを取得し、エンティティを操作し、更新されたエンティティをデータストアに戻す必要があります。 データストアは、エンティティの永続性を管理します。 エンティティはデータストアを知らないので、エンティティは自身を永続化する方法を知らない。
インタラクターもエンティティを永続化する方法を知るべきではない。 インターアクターは、データストアとの対話を容易にするために、データマネージャと呼ばれるタイプのオブジェクトを使用したい場合があります。 データ マネージャは、フェッチ要求の作成、クエリの構築など、ストア固有の操作の多くを処理します。 これにより、インタラクターはアプリケーションロジックに集中することができ、 エンティティがどのように収集され、永続化されるかを知る必要はありません。 データマネージャーを使用する意味がある場合の一例として、後述するCore Dataを使用する場合があります。
Here’s the interface for the example app’s data manager:
@interface VTDListDataManager : NSObject@property (nonatomic, strong) VTDCoreDataStore *dataStore;- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;@end
TDD を使用して Interactor を開発する場合、本番データ ストアをテストのダブル/モックで置き換えることが可能です。 リモート サーバー (Web サービス) に接続したり、ディスク (データベース) に触れたりしないため、テストはより速く、より再現性が高くなります。
データストアを明確な境界を持つ層として維持する理由の 1 つは、特定の永続性テクノロジーを選択するのを遅らせることができることです。 データ ストアが単一のクラスである場合、基本的な永続化戦略でアプリケーションを開始し、SQLite または Core Data に後でアップグレードすることができます。 しかし、VIPER で Core Data を使用することは、これまでで最高の Core Data 体験となり得ます。 Core Data は、高速なアクセスと低いメモリフットプリントを維持しながらデータを永続化するための素晴らしいツールです。 しかし、アプリケーションの実装ファイルのいたるところ、特に必要のないところでNSManagedObjectContext
を巻きつける癖があります。 VIPER は Core Data をあるべき場所、つまりデータストアレイヤーに保持します。
ToDo リストの例では、Core Data が使用されていることを知るアプリの 2 つの部分は、Core Data スタックを設定するデータ ストア自身と、データ マネージャーだけです。 データ マネージャーはフェッチ リクエストを実行し、データ ストアから返された NSManagedObjects
を標準の PONSO モデル オブジェクトに変換して、ビジネス ロジック レイヤーに渡します。
Core Data ストアにアクセスするために要求が行われたとき、データ マネージャーの内部はどのように見えるかについて説明します。 ストーリーボードには多くの便利な機能があり、完全に無視することは間違いです。 しかし、ストーリーボードが提供するすべての機能を採用しながら、VIPER のすべての目標を達成することは困難です。
私たちが行う妥協点は、セグメンテーションを使用しないことを選択する傾向があることです。 セグエを使用することが理にかなっている場合もありますが、セグエの危険性は、画面間の分離、および UI とアプリケーション ロジックの分離を維持することが非常に困難になることです。 経験則として、prepareForSegue メソッドの実装が必要な場合は、セグエを使用しないようにしています。
その他に、ストーリーボードは、特に自動レイアウトを使用中にユーザー インターフェイスのレイアウトを実装するための素晴らしい方法です。 9377>
static NSString *ListViewControllerIdentifier = @"VTDListViewController";@implementation VTDListWireframe- (void)presentListInterfaceFromWindow:(UIWindow *)window { VTDListViewController *listViewController = ; listViewController.eventHandler = self.listPresenter; self.listPresenter.userInterface = listViewController; self.listViewController = listViewController; ;}- (VTDListViewController *)listViewControllerFromStoryboard { UIStoryboard *storyboard = ; VTDListViewController *viewController = ; return viewController;}- (UIStoryboard *)mainStoryboard { UIStoryboard *storyboard = ]; return storyboard;}@end
Using VIPER to Build Modules
VIPER で作業していると、しばしば、画面または画面のセットがモジュールとしてまとまる傾向があることに気づかされます。 モジュールはいくつかの方法で説明できますが、通常は機能として考えるのが最もよいでしょう。 ポッドキャスティングのアプリでは、モジュールは、オーディオプレーヤーや購読ブラウザーになるかもしれません。 私たちの ToDo リスト アプリでは、リスト画面と追加画面はそれぞれ別のモジュールとして構築されています。
一連のモジュールとしてアプリを設計することには、いくつかの利点があります。 1 つは、モジュールが非常に明確でよく定義されたインターフェイスを持ち、他のモジュールから独立していることです。
To-do リストの例では、モジュール間の分離を非常に明確にしたかったので、add モジュールの 2 つのプロトコルを定義しました。 1 つはモジュール インターフェイスで、モジュールができることを定義します。 もうひとつはモジュールデリゲートで、モジュールが何をしたかを記述します。 例:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;@end@protocol VTDAddModuleDelegate <NSObject>- (void)addModuleDidCancelAddAction;- (void)addModuleDidSaveAddAction;@end
モジュールはユーザーにとって価値のあるものであるために提示されなければならないので、モジュールの Presenter は通常モジュールインターフェースを実装しています。 他のモジュールがこれを提示したい場合、そのPresenterはモジュール委任プロトコルを実装し、提示されている間にモジュールが何をしたかを知ることができる。
モジュールは、複数の画面に使用できるエンティティ、インタラクタ、マネージャの共通アプリケーションロジック層を含むかもしれない。 これはもちろん、これらの画面間の相互作用と、それらがどの程度類似しているかに依存します。 モジュールは、ToDoリストの例に示すように、単一の画面のみを表すことも同様に簡単にできる。 この場合、アプリケーション ロジック レイヤーはその特定のモジュールの動作に非常に特化することができます。
モジュールはまた、コードを整理するための良いシンプルな方法です。 モジュールのすべてのコードを Xcode の独自のフォルダーとグループに隠しておくと、何かを変更する必要があるときに簡単に見つけることができます。 9377>
VIPER でモジュールを構築するもう 1 つの利点は、複数のフォーム ファクターへの拡張がより簡単になることです。 すべてのユースケースのアプリケーション ロジックを Interactor レイヤーで分離することにより、アプリケーション レイヤーを再利用しながら、タブレット、電話、または Mac 用の新しいユーザー インターフェイスの構築に集中することができます。 この場合、iPad の画面は「スーパー」プレゼンターとワイヤーフレームで表現され、iPhone 用に書かれた既存のプレゼンターとワイヤーフレームを使用して画面を構成することになります。 複数のプラットフォームにわたってアプリを構築し維持することは非常に困難ですが、モデルとアプリケーション レイヤーにわたって再利用を促進する優れたアーキテクチャは、これをはるかに容易にします。
VIPER によるテスト
VIPERに従って、TDD を採用しやすくする関心事の分離を促進する。 インターアクターには、UI から独立した純粋なロジックが含まれているため、テストでの駆動が容易です。 Presenterには、表示するためのデータを準備するロジックが含まれ、UIKitのウィジェットに依存しません。 このロジックを開発することも、テストによる駆動が容易です。
私たちが推奨する方法は、Interactor から開始することです。 UI のすべては、ユースケースのニーズに応えるために存在します。 TDD を使用してインタラクター用の API をテスト ドライブすることにより、UI とユースケースの間の関係をよりよく理解することができます。
例として、今後の ToDo 項目のリストを担当するインタラクターを見てみましょう。 今後の項目を見つけるための方針は、来週末までに期限のあるすべてのTo-Do項目を見つけ、各To-Do項目を今日、明日、今週末、または来週に期限を迎えるものとして分類することです。
最初に書くテストは、Interactor が来週末までに期限のあるすべての ToDo 項目を見つけることを確認することです。
- (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek{ todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY]; ;}
いったん Interactor が適切な ToDo 項目を求めることがわかったら、いくつかのテストを書いて、正しい相対日付グループ(たとえば
- (void)testFindingUpcomingItemsWithOneItemDueTodayReturnsOneUpcomingItemsForToday{ NSArray *todoItems = @]; ; NSArray *upcomingItems = @]; ; ;}
Interactor の API がどのようなものかわかったので、Presenter を開発することができます。 PresenterがInteractorから今後のToDo項目を受け取るとき、データを適切にフォーマットしてUIに表示することをテストします:
- (void)testFoundZeroUpcomingItemsDisplaysNoContentMessage{ showNoContentMessage]; ];}- (void)testFoundUpcomingItemForTodayDisplaysUpcomingDataWithNoDay{ VTDUpcomingDisplayData *displayData = ; showUpcomingDisplayData:displayData]; NSCalendar *calendar = ; NSDate *dueDate = ; VTDUpcomingItem *haircut = ; ];}- (void)testFoundUpcomingItemForTomorrowDisplaysUpcomingDataWithDay{ VTDUpcomingDisplayData *displayData = ; showUpcomingDisplayData:displayData]; NSCalendar *calendar = ; NSDate *dueDate = ; VTDUpcomingItem *groceries = ; ];}
また、ユーザーが新しいToDo項目を追加したいときにアプリが適切な動作を開始することをテストします:
- (void)testAddNewToDoItemActionPresentsAddToDoUI{ presentAddInterface]; ;}
次にビューを開発できます。 今後の ToDo アイテムがない場合、特別なメッセージを表示します。
- (void)testShowingNoContentMessageShowsNoContentView{ ; XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view");}
表示する今後の ToDo アイテムがある場合、テーブルが表示されていることを確認します。 Interactor を最初に開発し、次に Presenter を開発すると、これらのレイヤーのテスト スイートを最初に構築し、これらのユース ケースを実装するための基礎を築くことができます。 UIを操作してテストする必要がないので、これらのクラスを迅速に反復することができます。 そして、Viewを開発する際には、ロジックとプレゼンテーションレイヤーが動作し、テストされた状態でViewに接続することができます。 ビューの開発が完了する頃には、アプリケーションを初めて実行したときに、すべてのテストが合格しているため、すべてが正しく動作していることに気づくかもしれません。 皆さんの多くは、次にどこへ行けばよいのか悩んでいるかもしれません。 この記事と VIPER を使用したアプリの実装例は、できる限り具体的で明確に定義されたものです。 私たちのToDoリストアプリはどちらかというとわかりやすいものですが、VIPERを使用してアプリを構築する方法を正確に説明しているはずです。 実際のプロジェクトでは、この例にどれだけ忠実に従うかは、あなた自身の課題と制約に依存します。 私たちの経験では、各プロジェクトで VIPER を使用するアプローチは若干異なりますが、すべてのプロジェクトで、VIPER を使用してアプローチを導くことにより大きな恩恵を受けています。 たとえば、「ウサギ」オブジェクトの巣窟に遭遇したり、Storyboards でセグエを使用することでアプリが恩恵を受けるかもしれません。 それでもかまいません。 このような場合、VIPERが象徴する精神を考慮して決定してください。 VIPERの核心は、「単一責任原則」に基づくアーキテクチャです。
また、既存のアプリで VIPER を使用できないかと考えているかもしれません。 このシナリオでは、VIPER を使用して新しい機能を構築することを検討します。 私たちの既存のプロジェクトの多くは、このルートを取っています。 これにより、VIPER を使用してモジュールを構築することができ、また、単一責任原則に基づくアーキテクチャの採用を困難にするような既存の問題を発見するのに役立ちます。
ソフトウェア開発における素晴らしいことの 1 つは、すべてのアプリが異なり、また、あらゆるアプリをアーキテクチャする異なる方法が存在することです。 これは、私たちにとって、すべてのアプリが、新しいことを学び、試す新しい機会であることを意味します。 VIPERを試していただければ、あなたも新しいことをいくつか学べると思います。
Swift の補足
先週の WWDC で Apple は、Cocoa および Cocoa Touch 開発の未来として Swift プログラミング言語を紹介しました。 Swift 言語について複雑な意見を形成するには時期尚早ですが、言語がソフトウェアの設計と構築の方法に大きな影響を与えることは分かっています。 私たちは、VIPERにとってこれが何を意味するのかを学ぶために、Swiftを使ってVIPER TODOのサンプルアプリを書き直すことにしました。 今のところ、私たちは見ているものを気に入っています。 以下は、VIPER を使用してアプリを構築する経験を向上させると感じる Swift のいくつかの機能です。
Structs
VIPER では、プレゼンターからビューへのように、層間でデータを渡すために小さく、軽量のモデル クラスを使用します。 これらの PONSO は通常、単に少量のデータを運ぶことを目的としており、通常はサブクラス化されることを意図していません。 Swift の構造体はこのような状況に完全に適合します。 以下は、VIPER Swiftの例で使用される構造体の例です。 この構造体は等価である必要があることに注意してください、そして、その型の 2 つのインスタンスを比較するために == 演算子をオーバーロードしています:
struct UpcomingDisplayItem : Equatable, Printable { let title : String = "" let dueDate : String = "" var description : String { get { return "\(title) -- \(dueDate)" }} init(title: String, dueDate: String) { self.title = title self.dueDate = dueDate }}func == (leftSide: UpcomingDisplayItem, rightSide: UpcomingDisplayItem) -> Bool { var hasEqualSections = false hasEqualSections = rightSide.title == leftSide.title if hasEqualSections == false { return false } hasEqualSections = rightSide.dueDate == rightSide.dueDate return hasEqualSections}
Type Safety
おそらく Objective-C と Swift の最大の違いは、両者が型を扱う方法です。 Objective-C は動的に型付けされ、Swift は、コンパイル時に型チェックを実装する方法について非常に意図的に厳密です。 VIPERのように、アプリが複数の異なるレイヤーで構成されているアーキテクチャでは、型安全性はプログラマの効率とアーキテクチャ構造のための大きな勝利となり得ます。 コンパイラは、コンテナやオブジェクトがレイヤーの境界間で受け渡されるときに、正しい型であることを確認する手助けをしてくれます。 これは、上に示したような構造体を使用するのに最適な場所です。 構造体が2つのレイヤーの境界に存在する場合、型安全性のおかげで、構造体がレイヤーの間から抜け出すことができないことが保証されます。
Further Reading
- VIPER TODO, article example app
- VIPER SWIFT, article example app built using Swift
- Counter, 別のアプリ例
- Mutual Mobile VIPER入門
- Clean Architecture
- Light View Controllers
- Testing View Controllers
- Bunnies