MVVM vs TCA 比較

Model-View-ViewModel:直感的、柔軟、広く使われている

VS
TCA

The Composable Architecture:関数型、テスト可能、予測可能

10 分で読了iOS

スコア比較

グラフを読み込み中...

詳細スコア

詳細スコア: MVVM TCA — カテゴリー別10点満点のスコア
カテゴリーMVVMTCA
パフォーマンス
8/10
8/10
学習のしやすさ
9/10
4/10
エコシステム
9/10
7/10
コミュニティ
9/10
7/10
求人市場
10/10
7/10
将来性
8/10
9/10

長所と短所

MVVM

長所

  • 学習しやすい——概念がシンプルで、コミュニティのリソースが豊富
  • SwiftUIとの自然な親和性——@Observable、@ObservedObjectとシームレスに連携
  • 柔軟——プロジェクトの規模とニーズに合わせて形作れる
  • テスト容易性が高い——ViewModelをViewから独立してテスト可能
  • あらゆる規模のプロジェクトに適する
  • チームが容易に適応できる——iOS以外でも知られたパターン
  • CombineまたはAsync/awaitと互換性がある

短所

  • 大規模プロジェクトでは「Massive ViewModel」問題に陥りやすい
  • 標準的な実装がない——チームごとに解釈が異なる
  • 副作用(side effects)管理の明確なルールが定義されていない
  • 調整(ナビゲーション、ディープリンク)には追加のパターンが必要(Coordinator)
  • 状態の一貫性を保つのは手動作業になりがちでエラーを招きやすい

最適な用途

小〜中規模のプロジェクトチームが素早く生産的になる必要があるプロジェクトアーキテクチャの経験が少ないチームSwiftUI + CombineまたはAsync/awaitのプロジェクト標準的なエンタープライズアプリケーション

TCA

長所

  • 単方向データフロー——状態が完全に予測可能
  • 網羅的テスト——すべてのreducer、effect、dependencyをテスト可能
  • コンポジション——大きなfeatureを小さなReducerから組み立てる
  • 副作用(Effect)が完全に制御・テスト可能
  • Point-Freeチームによる活発な開発と優れたドキュメント
  • 依存性注入がフレームワーク内で標準化されている
  • SwiftUI NavigationStack統合(ツリーベースのナビゲーション)

短所

  • 高い学習曲線——State、Action、Reducer、Effect、Storeの概念
  • ボイラープレート——シンプルな機能でもAction enumとReducerが必要
  • 小規模プロジェクトには過剰に複雑(オーバーエンジニアリングのリスク)
  • コンパイル時間——大規模プロジェクトではビルド時間が長くなることがある
  • チーム全員がTCAを理解する必要がある——混在した使い方は問題を引き起こす

最適な用途

大規模で複雑なアプリケーションテストカバレッジが重要なプロジェクト複雑なナビゲーションとディープリンクの要件関数型プログラミングの経験があるチーム複数チームによる長期的なエンタープライズプロジェクト

コード比較

MVVM
// MVVM - 商品リスト

import SwiftUI
import Observation
@Observable
class ProductListViewModel {
    var products: [Product] = []
    var isLoading = false
    var errorMessage: String?
    var searchText = ""

    private let repository: ProductRepository

    init(repository: ProductRepository = .live) {
        self.repository = repository
    }

    var filteredProducts: [Product] {
        guard !searchText.isEmpty else { return products }
        return products.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
    }

    func loadProducts() async {
        isLoading = true
        errorMessage = nil
        do {
            products = try await repository.fetchProducts()
        } catch {
            errorMessage = "Ürünler yüklenemedi: \(error.localizedDescription)"
        }
        isLoading = false
    }

    func deleteProduct(_ product: Product) async {
        do {
            try await repository.delete(product.id)
            products.removeAll { $0.id == product.id }
        } catch {
            errorMessage = "Silme başarısız: \(error.localizedDescription)"
        }
    }
}

struct ProductListView: View {
    @State private var viewModel = ProductListViewModel()

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading {
                    ProgressView("Yükleniyor...")
                } else {
                    List(viewModel.filteredProducts) { product in
                        ProductRow(product: product)
                    }
                    .searchable(text: $viewModel.searchText)
                }
            }
            .navigationTitle("Ürünler")
        }
        .task { await viewModel.loadProducts() }
        .alert("Hata", isPresented: .constant(viewModel.errorMessage != nil)) {
            Button("Tamam") { viewModel.errorMessage = nil }
        } message: {
            Text(viewModel.errorMessage ?? "")
        }
    }
}
TCA
// TCA - 商品リスト

import ComposableArchitecture
import SwiftUI
@Reducer
struct ProductListFeature {
    @ObservableState
    struct State: Equatable {
        var products: [Product] = []
        var isLoading = false
        var errorMessage: String?
        var searchText = ""

        var filteredProducts: [Product] {
            guard !searchText.isEmpty else { return products }
            return products.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
        }
    }

    enum Action {
        case onAppear
        case searchTextChanged(String)
        case deleteProduct(id: String)
        case productsLoaded(Result<[Product], Error>)
        case productDeleted(Result<Void, Error>)
        case dismissError
    }

    @Dependency(\.productRepository) var repository

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .onAppear:
                state.isLoading = true
                return .run { send in
                    await send(.productsLoaded(
                        Result { try await repository.fetchProducts() }
                    ))
                }

            case .searchTextChanged(let text):
                state.searchText = text
                return .none

            case .deleteProduct(let id):
                return .run { send in
                    await send(.productDeleted(
                        Result { try await repository.delete(id) }
                    ))
                }

            case .productsLoaded(.success(let products)):
                state.isLoading = false
                state.products = products
                return .none

            case .productsLoaded(.failure(let error)):
                state.isLoading = false
                state.errorMessage = error.localizedDescription
                return .none

            case .productDeleted(.success):
                return .send(.onAppear)

            case .productDeleted(.failure(let error)):
                state.errorMessage = error.localizedDescription
                return .none

            case .dismissError:
                state.errorMessage = nil
                return .none
            }
        }
    }
}

struct ProductListView: View {
    let store: StoreOf<ProductListFeature>

    var body: some View {
        WithPerceptionTracking {
            NavigationStack {
                List(store.filteredProducts) { product in
                    Text(product.name)
                }
                .searchable(text: store.binding(get: \.searchText, send: ProductListFeature.Action.searchTextChanged))
                .navigationTitle("Ürünler")
            }
            .task { store.send(.onAppear) }
        }
    }
}

結論

小〜中規模のプロジェクトにはMVVM——開発が速く、参入障壁が低い。大規模で複雑、テストが重要なプロジェクトにはTCA——予測可能な状態管理とコンポーザブルなアーキテクチャの価値が長期的に発揮される。ハイブリッドなアプローチも可能:主要機能はMVVM、重要/複雑なフローはTCA。

無料相談を受ける
FAQ

よくある質問

基本概念の理解には1〜2週間、生産性を発揮するには1〜2ヶ月。Point-Freeの動画とTCA examplesリポジトリを確認することをお勧めします。

関連ブログ記事

すべての記事を見る
すべての比較