VIPER(View-Interactor-Presenter-Entity-Router)— Mutual Mobile社がiOSアプリケーション向けに開発したモジュラーアーキテクチャです。VIPERはアプリケーションを5つの層に分割します。Viewは表示、Interactorはビジネスロジック、Presenterはデータ準備、Entityはデータモデル、Routerはモジュール間のナビゲーションを担当します。VIPERはモバイルアーキテクチャの中で単一責任原則を最も詳細に実装したものです。詳細はobjc.ioの記事をご覧ください。
重要ポイント
VIPER(View-Interactor-Presenter-Entity-Router)— 2013〜2014年にMutual Mobileで大規模iOSプロジェクト向けに開発されたアーキテクチャパターンです。アプリケーションの各画面は、厳密に定義された責任を持つ5つのコンポーネントからなる独立したモジュールです。VIPERはモバイル開発における単一責任原則(Single Responsibility Principle)の最も厳格な実装です。どのコンポーネントも、他のコンポーネントができることを行いません。
Viewはパッシブなコンポーネントで、Presenterから渡されたデータの表示のみを担当します。Viewはビジネスロジックを含まず、ナビゲーションを処理せず、ネットワークリクエストを行いません。iOSでは — ViewProtocolを持つUIViewControllerです。InteractorはEntityやサービス(ネットワーク、DB、GPS)と連携するビジネスロジック層です。InteractorはUIKitをインポートしません。PresenterはViewとInteractorの間の仲介者です。Interactorからデータを受け取り、表示用にフォーマットしてViewに渡します。PresenterもUIKitをインポートしません。Entity — データモデル(struct、class)。Router — ナビゲーションを管理し、モジュールを作成し、画面を開き、モジュール間でデータを渡します。
| コンポーネント | 責任 | 依存関係 |
|---|---|---|
| View | 表示、アニメーション、ジェスチャー | UIKit(Viewのみ) |
| Interactor | ビジネスロジック、ネットワーク、DB | Entity、サービス |
| Presenter | データフォーマット、Viewコマンド | ViewProtocol、Interactor |
| Entity | データモデル | なし |
| Router | ナビゲーション、モジュール作成 | UIViewController(遷移用) |
コンポーネント間の関係はプロトコルによって記述されます。ViewProtocolは表示メソッドを、InteractorProtocolはビジネスロジックメソッドを、PresenterProtocolはイベント処理メソッドを、RouterProtocolはナビゲーションメソッドを定義します。各コンポーネントはプロトコルを介してのみ通信し、実装の容易な置き換えと分離テストを可能にします。平均して、1画面のVIPERモジュールには5つのプロトコル+5つのクラス+1つのBuilder/Assembler=11ファイルが含まれます。
VIPERモジュールの構築はBuilder(またはAssembler)で行われ、5つのコンポーネントすべてを作成し、プロトコルを介して接続します。Builderはコンポーネントが互いの具象型を知っている唯一の場所です。アセンブリ後、Viewは表示のために外部に返され、残りのチェーンはクリーンで分離してテストされます。
// プロトコル — View
protocol UserViewProtocol: AnyObject {
func display(name: String)
func display(email: String)
func showLoading()
func hideLoading()
}
// プロトコル — Interactor
protocol UserInteractorProtocol {
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
}
// プロトコル — Router
protocol UserRouterProtocol {
func navigateToProfile(userId: Int)
}
// Interactor — ビジネスロジック
final class UserInteractor: UserInteractorProtocol {
private let service: UserService
init(service: UserService) { self.service = service }
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
service.fetchUser(id: id, completion: completion)
}
}
// Presenter — データ準備
final class UserPresenter {
private weak var view: UserViewProtocol?
private let interactor: UserInteractorProtocol
private let router: UserRouterProtocol
init(interactor: UserInteractorProtocol, router: UserRouterProtocol) {
self.interactor = interactor
self.router = router
}
func setView(_ view: UserViewProtocol) {
self.view = view
}
func viewDidLoad() {
view?.showLoading()
interactor.fetchUser(id: 42) { [weak self] result in
guard let self else { return }
self.view?.hideLoading()
switch result {
case .success(let user):
self.view?.display(name: user.name)
self.view?.display(email: user.email)
case .failure(let error):
// エラー処理
}
}
}
}
// Router — ナビゲーション
final class UserRouter: UserRouterProtocol {
private weak var viewController: UIViewController?
func setViewController(_ vc: UIViewController) {
viewController = vc
}
func navigateToProfile(userId: Int) {
let profileModule = ProfileModuleBuilder.build(userId: userId)
viewController?.navigationController?.pushViewController(profileModule, animated: true)
}
}
// Builder — モジュール構築
enum UserModuleBuilder {
static func build(userId: Int) -> UIViewController {
let service = UserService()
let interactor = UserInteractor(service: service)
let router = UserRouter()
let presenter = UserPresenter(interactor: interactor, router: router)
let viewController = UserViewController(presenter: presenter)
presenter.setView(viewController)
router.setViewController(viewController)
return viewController
}
}
Builder/AssemblerはVIPERの重要な要素であり、依存性注入を手動で実装します。コンストラクタを介した依存性注入(constructor injection)により、コンポーネントが依存関係なしに作成されることを防ぎます。現代のVIPERでは、BuilderはSwinject(DIコンテナ)を使用できますが、手動アセンブリはテストの透過性が高くなります。IT Sectrでは、複雑なロジックを持つモジュールには手動アセンブリのVIPERを適用しています。これにより、新規開発者のコード理解が容易になります。
Mapper(Formatter)はVIPERのオプションの6番目のコンポーネントです。MapperはEntity(DB/サーバーモデル)をViewModel(表示モデル)に変換します。Entityにはid、first_name、last_name、emailフィールドを持つUserDTOが含まれます。ViewModel — name(first_name + last_name)とemailを持つUserDisplayItem。MapperはPresenter内で実行されます。データマッピングが複雑な場合(複数のEntity→1つのViewModel)、Mapperはテスト用に別のクラスに抽出されます。
VIPERモジュールは分離されており、互いについて知りません。モジュール間の通信はRouterを介して行われます。ユーザーがユーザー画面の「プロフィール」ボタンをクリックすると、Presenterはrouter.navigateToProfile(userId: 42)を呼び出します。RouterはProfileModuleBuilder.build(userId: 42)を介して新しいモジュールを作成し、navigationController.pushを介して開きます。データフロー:モジュールA → Router A → モジュールB Builder → モジュールBが作成され開かれます。
データの返却(例:選択画面で都市を選択→プロフィール編集画面に戻る)は、VIPERではデリゲートまたはクロージャを介して実装されます。モジュールBはdidSelectCity(_ city: City)メソッドを持つModuleBDelegateプロトコルを定義します。モジュールAはこのプロトコルを実装します。Router AはデリゲートをモジュールB Builderに渡します。都市が選択されると、モジュールBはdelegate?.didSelectCity(city)を呼び出します。これは標準的なiOSのプラクティスであり、UIKit開発者にはおなじみです。
| シナリオ | メカニズム | 例 |
|---|---|---|
| 前方ナビゲーション | Router → Builder | navigateToProfile(userId:) |
| データの返却 | Delegate | didSelectCity(_:) |
| システム通知 | NotificationCenter | UserDidLogout |
| Interactorからのイベント | Presenter → View | WebSocketメッセージ |
NotificationCenterは、複数のモジュールに同時に影響するシステムイベント(ログアウト、プラン変更、プッシュ通知)に使用されます。RouterまたはAppDelegateがNotificationを購読し、必要なモジュールを作成するか状態を更新します。VIPERはNotificationCenterを禁止していません。重要なのは、1対多のイベントにのみ使用し、1対1の通信にはデリゲートまたはクロージャを使用することです。
VIPER vs MVVM — VIPERは画面ごとに2〜3倍のコードが必要ですが、絶対的なコンポーネントの分離を提供します。ViewModel + SwiftUIを使用したMVVMはよりシンプルで高速ですが、5人以上の開発者チームではスケーラビリティが低下します。VIPERは誰が何に責任を持つかを厳密に定義します。Interactor — ビジネスロジックのみ、Presenter — フォーマット、Router — ナビゲーション。MVVMでは、ViewModelがしばしば肥大化し、ナビゲーションとビジネスロジックを引き継ぎます。
VIPER vs Clean Architecture — VIPERはiOS UIKitに適応したClean Architectureの特定のケースです。Interactor = Use Case、Entity = Domain Model、Presenter = Presentation、Router = Controller(Robert Martinの用語)。Clean ArchitectureはInteractorとデータの間にGateway/Repository層を追加しますが、VIPERでは通常分離されません。現代のSwiftUIプロジェクトでは、ほとんどのチームがClean Architecture(The Composable Architecture)またはMVVMを選択し、VIPERはUIKitレガシーに残します。
VIPERを選ぶべき時 — 5人以上の開発者チーム、50画面以上のUIKitプロジェクト、80%以上のテスト要件、iOSのみ(VIPERは書き換えなしではAndroidに移植できません)。VIPERは予測可能な構造を提供します。新しい開発者は15分でモジュールを理解できます。ただし、ファイル数が多いため、開発速度はMVVMと比較して20〜30%低下します。IT Sectrでは、3人以上のチームによるエンタープライズUIKitプロジェクトにはVIPERを使用し、新しいSwiftUIプロジェクトにはClean Architectureを優先しています。
VIPERはテスト用に設計されています — 各コンポーネントはプロトコルを介して分離してテストされます。Interactorはモックサービスでテストされ、fetchUserが正しいIDで呼び出され、結果がPresenterに渡されることを確認します。PresenterはモックのViewとInteractorオブジェクトでテストされます。Routerはモックナビゲーションでテストされ、navigateToProfileが正しいuserIdで呼び出され、正しいモジュールが作成されることを確認します。ViewはUIテスト(XCUITest)でテストされます。
import XCTest
final class UserPresenterTests: XCTestCase {
func testViewDidLoad_callsFetchUserAndUpdatesView() {
// Given
let view = MockUserView()
let interactor = MockUserInteractor()
let router = MockUserRouter()
let presenter = UserPresenter(interactor: interactor, router: router)
presenter.setView(view)
let expectedUser = User(id: 42, name: "John", email: "john@test.com")
interactor.result = .success(expectedUser)
// When
presenter.viewDidLoad()
// Then
XCTAssertEqual(interactor.capturedUserId, 42)
XCTAssertEqual(view.displayedName, "John")
XCTAssertTrue(view.didShowLoading)
}
}
モックオブジェクトはVIPER用に手動で作成されるか(キャプチャプロパティを格納するクラス)、Cuckoo / Mockingbirdなどのライブラリを介して作成されます。手動モッククラスはよりシンプルで明確であり、特に新しい開発者のトレーニングに適しています。各モックはキャプチャされた値(capturedUserId、displayedName)と呼び出しフラグ(didShowLoading)を格納します。テストの終了時には、メソッドが呼び出されたかだけでなく、どのパラメータで呼び出されたかも確認されます。これにより、データフローの正確性に自信が持てます。
コードカバレッジはIT SectrのVIPERプロジェクトで、Interactorが85〜95%、Presenterが90〜95%、Routerが70〜80%、Viewが30〜50%(UIテスト経由)に達します。Viewはスナップショットテスト(SnapshotTesting、1.5Kスター)でテストされます。これはXCUITestより高速で、より多くのケースをカバーします。VIPERプロジェクトの全体的なカバレッジは通常70〜80%で、MVVMプロジェクト(50〜65%)より高いですが、テスト作成により多くの時間が必要です(開発時間の30〜40%対MVVMの20〜25%)。
よくある質問
最低11ファイル:5つのプロトコル(ViewProtocol、InteractorProtocol、PresenterProtocol、RouterProtocol、Entity)、5つの実装(ViewController、Interactor、Presenter、Router、Entity)、およびBuilder/Assembler。Mapper(Formatter)を含めると12〜13ファイル。50画面のプロジェクトでは、VIPERモジュールだけで550〜650ファイルになります。MVVMでは画面ごとに3ファイル(ViewModel、View、Model)が必要で、50画面で150ファイルです。
はい、理論的にはVIPERはAndroidに移植可能ですが、実際には使用されていません。GoogleはJetpackを使用したMVVMを推奨しています。VIPERはiOS UIKit用に作られ、ViewControllerはライフサイクルのためにテストが困難でした。Androidでは、Jetpack ViewModelがVIPERの分離なしでテスト問題を解決します。AndroidでのVIPER相当は、モジュール/機能分割を用いたClean Architectureです。
VIPERはClean ArchitectureのiOS固有の実装です。InteractorはUse Case、EntityはDomain Model、PresenterはPresentation層に対応します。Clean ArchitectureはInteractorとデータの間にRepository/Gatewayを追加しますが、VIPERでは通常Interactor内部に実装されます。Clean ArchitectureはRouterを規定せず、ナビゲーションは実装に委ねられます。
いいえ — SwiftUIはMVVM + Combine用に設計されています。SwiftUIのVIPERは冗長です。宣言型UIで画面ごとに5つのコンポーネントは、利点のないオーバーヘッドです。SwiftUIの場合は、MVVMまたはTCA(The Composable Architecture)を選択してください。VIPERはUIKitレガシーおよびiOS 12以下を最小バージョンとするプロジェクトに関連し続けています。
Routerを介して行います。モジュールAはrouter.navigateToProfile(userId: id)を呼び出します。Router AはBuilderを介してモジュールBを作成し、userIdを渡します。コールバック通信はデリゲートを介して行います。モジュールBがModuleBDelegateプロトコルを定義し、モジュールAがそれを実装してRouterを介して渡します。システムイベント(ログアウト)はNotificationCenterを介して行います。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。