一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • iOS開発のアーキテクチャ選定:MVC・MVVM・TCAの比較と実践

iOSアーキテクチャの変遷

iOS開発におけるアーキテクチャの議論は、プラットフォームの成熟とともに進化を続けてきた。初期のiOS開発ではAppleが推奨するMVC(Model-View-Controller)が標準でしたが、アプリケーションの複雑化に伴い「Massive View Controller」と揶揄されるほどViewControllerが肥大化する問題が顕在化した。その後、MVVM(Model-View-ViewModel)がリアクティブプログラミングの台頭とともに普及し、近年ではSwiftUIとの親和性が高いTCA(The Composable Architecture)が注目を集めています。

本記事では、これら3つのアーキテクチャを実装例とともに比較し、プロジェクトに応じた選定の指針を示す。

MVC:Apple標準のシンプルさ

基本構造

MVCはAppleがUIKitとともに長年推奨してきましたアーキテクチャです。ModelがデータとビジネスロジックをViewが画面表示を、Controllerがその仲介を担当します。UIKitにおいてはUIViewControllerがControllerの役割を果たす。

// Model
struct Task: Identifiable {
let id: UUID
var title: String
var isCompleted: Bool
}

// Controller
class TaskListViewController: UIViewController, UITableViewDataSource {
private var tasks: [Task] = []
private let tableView = UITableView()

override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
loadTasks()
}

private func loadTasks() {
// APIやDBからタスクを取得
tasks = TaskRepository.shared.fetchAll()
tableView.reloadData()
}

func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return tasks.count
}

func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let task = tasks[indexPath.row]
cell.textLabel?.text = task.title
cell.accessoryType = task.isCompleted ? .checkmark : .none
return cell
}
}

メリットとデメリット

MVCの最大のメリットはシンプルさです。新しいフレームワークやライブラリの導入が不要で、Appleの公式ドキュメントやサンプルコードもMVCで書かれていることが多いです。小規模なアプリケーションや、プロトタイプの迅速な開発には適しています。

しかし、アプリケーションが成長するとViewControllerにロジックが集中し、テストが困難になります。データの取得、変換、表示、ユーザーインタラクションの処理がすべてViewControllerに詰め込まれ、数百行から千行を超えるファイルになることも珍しくありません。

MVVM:テスタビリティの向上

基本構造

MVVMはViewControllerからビジネスロジックを切り出し、ViewModelという層に移すアーキテクチャです。SwiftUIではCombineや@Observableマクロとの組み合わせが自然で、データバインディングにより状態変化を自動的にUIに反映できます。

// ViewModel
@Observable
class TaskListViewModel {
private(set) var tasks: [Task] = []
private let repository: TaskRepositoryProtocol

init(repository: TaskRepositoryProtocol = TaskRepository.shared) {
self.repository = repository
}

func loadTasks() {
tasks = repository.fetchAll()
}

func toggleCompletion(for task: Task) {
guard let index = tasks.firstIndex(where: { $0.id == task.id }) else { return }
tasks[index].isCompleted.toggle()
repository.update(tasks[index])
}

var completedCount: Int {
tasks.filter(\.isCompleted).count
}
}

// View (SwiftUI)
struct TaskListView: View {
@State private var viewModel = TaskListViewModel()

var body: some View {
NavigationStack {
List(viewModel.tasks) { task in
HStack {
Text(task.title)
Spacer()
if task.isCompleted {
Image(systemName: "checkmark")
}
}
.onTapGesture {
viewModel.toggleCompletion(for: task)
}
}
.navigationTitle("タスク (\(viewModel.completedCount)件完了)")
.onAppear { viewModel.loadTasks() }
}
}
}

メリットとデメリット

MVVMの最大の強みはテスタビリティです。ViewModelはUIフレームワークに依存しない純粋なSwiftクラスですため、XCTestで容易にユニットテストを書ける。プロトコルを使った依存性注入により、モックを使ったテストも自然に行える。

一方で、MVVMにはいくつかの課題もあります。ViewModelの責務の境界が曖昧になりがちで、チーム内で「何をViewModelに置くか」のコンセンサスが取れていないと、結局ViewModelが肥大化します。また、画面間のデータ共有や副作用の管理について、MVVMだけでは明確な指針がないため、プロジェクトごとに独自のルールを設ける必要があります。

TCA:関数型アプローチの徹底

基本構造

TCA(The Composable Architecture)はPoint-Free社が開発したSwift向けのアーキテクチャフレームワークです。Reduxにインスパイアされた単方向データフローを採用し、State、Action、Reducer、Effectという4つの概念でアプリケーションの構造を定義します。

import ComposableArchitecture

@Reducer
struct TaskListFeature {
@ObservableState
struct State: Equatable {
var tasks: IdentifiedArrayOf = []
}

enum Action {
case onAppear
case tasksLoaded([Task])
case toggleCompletion(Task.ID)
}

@Dependency(\.taskRepository) var repository

var body: some ReducerOf {
Reduce { state, action in
switch action {
case .onAppear:
return .run { send in
let tasks = try await repository.fetchAll()
await send(.tasksLoaded(tasks))
}
case let .tasksLoaded(tasks):
state.tasks = IdentifiedArray(uniqueElements: tasks)
return .none
case let .toggleCompletion(id):
state.tasks[id: id]?.isCompleted.toggle()
return .none
}
}
}
}

// View
struct TaskListView: View {
let store: StoreOf

var body: some View {
NavigationStack {
List(store.tasks) { task in
HStack {
Text(task.title)
Spacer()
if task.isCompleted {
Image(systemName: "checkmark")
}
}
.onTapGesture {
store.send(.toggleCompletion(task.id))
}
}
.navigationTitle("タスク")
.onAppear { store.send(.onAppear) }
}
}
}

メリットとデメリット

TCAの最大の強みは、状態管理の一貫性と予測可能性です。すべての状態変更はReducerを通じて行われるため、デバッグが容易であり、副作用(API通信、タイマーなど)もEffectとして明示的に管理されます。TestStoreを使ったテストは非常に強力で、アクションの送信から状態の変化、副作用の実行まで一連の流れを検証できます。

また、「Composable」の名が示す通り、小さなFeatureを組み合わせて大きなFeatureを構築する仕組みが整っています。画面遷移やモーダル表示もTCAのスコープ内で統一的に扱える。

デメリットとしては、学習コストの高さが挙げられます。関数型プログラミングの概念、単方向データフロー、Swiftの高度なジェネリクスなど、前提知識が多いです。また、TCA自体のバージョンアップに伴うAPIの変更も頻繁であり、追従のコストも考慮する必要があります。小規模なアプリケーションに対してはオーバーエンジニアリングになる可能性もあります。

選定の指針

アーキテクチャの選定は、プロジェクトの規模、チームの技術力、メンテナンス期間を総合的に考慮して判断すべきです。

  • 小規模・短期間のプロジェクト:MVCまたはシンプルなMVVMで十分。過度なアーキテクチャは開発速度を落とす
  • 中規模・チーム開発:MVVMが最もバランスが良い。テスタビリティを確保しつつ、学習コストも許容範囲内
  • 大規模・長期運用:TCAの構造的な強さが活きる。初期の学習コストを投資と割り切れるならば、長期的なメンテナビリティで大きなリターンがあります

筆者の経験では、チーム全員が同じアーキテクチャに対する理解度を持つことが最も重要です。いくら優れたアーキテクチャでも、チームメンバーが理解できなければ形骸化します。新しいアーキテクチャを導入する際は、ペアプログラミングやコードレビューを通じてチーム全体のスキルを底上げすることを強く推奨します。

まとめ

MVC、MVVM、TCAはそれぞれ異なるトレードオフを持つアーキテクチャです。重要なのは「最良のアーキテクチャ」を追い求めることではなく、プロジェクトとチームにとって「最適なアーキテクチャ」を選ぶことです。本記事の比較が、読者のアーキテクチャ選定の一助となれば幸いです。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア