SwiftUIでMV(Container/Presentation)パターンを実践して見えた課題と対応策

本投稿は TECOTEC Advent Calendar 2025 の1日目の記事です。

システム開発第二事業部の冨永です。

主にiOS/iPadアプリの開発を担当しております。

SwiftUIでの開発が一般化する中で、アーキテクチャ設計に悩む場面も増えてきました。 本記事では、実際に採用した MV(Container/Presentation)パターン について、 その導入背景と運用の中で見えてきた課題、そしてそれに対する対応策を紹介します。

目次

はじめに

SwiftUIでアプリを開発する際、アーキテクチャの選択は避けて通れないテーマです。 一般的には MVVM や TCA(The Composable Architecture) が主流として語られていますが、 最近では MV(Model-View)アーキテクチャ にも注目が集まっています。

MVアーキテクチャは、ViewとModelを直接つなぐシンプルな構成であり、 状態と描画の流れを明快に保てる点が特徴です。 ただし、実際に運用してみると、ロジックの責務分離、プレゼンテーションロジックの責務、状態管理の肥大化といった課題も見えてきました。

これらの課題を解消するために検討されたのが、 Container/Presentationパターン です。 このパターンはMVアーキテクチャをベースにしつつ、責務をもう一段階分離し、 Containerが状態管理・イベントハンドリングを担い、Presentationが純粋なUI描画に集中する構成をとります。

本記事では、MVアーキテクチャを実際に採用・運用した中で得られた知見と、 その課題を解決するためのContainer/Presentationパターンの導入、そして課題について解説します。

単なる構造の比較ではなく、 実際のプロジェクトでの実践的な課題と、 運用を通じて得た「現実的な設計上の判断ポイント」を共有したいと思います。

MVアーキテクチャとは

MV(Model-View)アーキテクチャは、UIと状態をシンプルに結びつける構成として、SwiftUIの思想と相性が良い設計パターンです。名前の通り、ModelView の2層構成でアプリを組み立てることを目的としています。

MVアーキテクチャの概要

MVアーキテクチャは、アプリケーションを「データ(Model)」と「見た目(View)」の2つのレイヤーに分け、ViewがModelの状態変化を直接監視する仕組みです。SwiftUIでは @State@Binding, @Environment, @StateObject,@ObservedObject@EnvironmentObject によってこの仕組みを自然に実現できます。

// MARK: Model

class UserModel: ObservableObject {
    @Published var name: String = "Alice"
    @Published var isPremium: Bool = true
}

// MARK: View

struct UserView: View {
    @ObservedObject var model: UserModel

    var body: some View {
        VStack {
            Text(model.isPremium ? "⭐️ \(model.name)" : model.name)
            Button("Toggle Premium") {
                model.isPremium.toggle()
            }
        }
    }
}

このように、ViewがModelを直接監視し、状態変化に応じて自動的に再描画が行われます。コード量が少なく、UI構築のトライアル&エラーがしやすいのが利点です。

MVVMとの違い

MVVM(Model-View-ViewModel)は、ViewとModelの間にViewModelを挟むことで責務を分離する構成です。一方でMVは、中間層を設けず ModelとViewを直接つなぐ ことで状態管理をシンプルにしています。

項目 MV MVVM
コード量
テスト容易性 限定的
SwiftUIとの親和性

MVVMは画面ごとに処理を分離しやすく、プレゼンテーションのテストがしやすいです。 一方で、各画面でModelが閉じている場合は問題になりませんが、複数画面で単一のModelを共有する場合に、Modelの受け渡しや状態共有の方法が複雑になりやすいという課題があります。 例えば、遷移時にViewModelへModelを注入する方法やRepositoryやSingletonを介して状態を保持する設計が求められます。

例:ユーザ一覧・ユーザ詳細のある画面のMVVMの例

import SwiftUI

// ===== MVVM:UserModelはSingleton、VMは画面ごとに分離 =====

// MARK: Model

@MainActor
final class UserModel: ObservableObject {
    static let shared = UserModel() // シングルトンで管理する場合の例
    @Published var users = ["Alice","Bob","Charlie"]
    private init() {}
}

// MARK: ViewModel

@MainActor
final class UserListViewModel: ObservableObject { // 画面毎にViewModelの定義が必要
    @Published private(set) var model = UserModel.shared // Modelの変化を監視する方法について工夫が必要
}

@MainActor
final class UserDetailViewModel: ObservableObject {
    @Published private(set) var model = UserModel.shared
    func rename(at index: Int, to new: String) { model.users[index] = new }
}

// MARK: View

/// ユーザ一覧View
struct MVVMUserListView: View {
    @StateObject private var vm = UserListViewModel()
    var body: some View {
        NavigationStack {
            List(vm.model.users.indices, id: \.self) { i in
                NavigationLink(vm.model.users[i]) { MVVMUserDetailView(index: i) }
            }
            .navigationTitle("Users (MVVM)")
        }
    }
}

/// ユーザ詳細View
struct MVVMUserDetailView: View {
    @StateObject private var vm = UserDetailVieViwModel()
    let index: Int
    var body: some View {
        VStack {
            Text(vm.model.users[index])
            Button("Rename to Bob") { vm.rename(at: index, to: "Bob") }
        }
        .navigationTitle("Detail")
    }
}

MVでのアプローチ

MVでは、Viewが直接Modelを監視するため、画面間でModelを共有する場合も 状態の引き渡しが明示的で、構造がシンプル になります。

import SwiftUI

// ===== MV:Modelを直接共有(直渡し) =====
@MainActor
final class UserModel: ObservableObject {
    @Published var users = ["Alice","Bob","Charlie"]
}

struct MVUserListView: View {
    @StateObject private var model = UserModel()
    var body: some View {
        NavigationStack {
            List(model.users.indices, id: \.self) { i in
                NavigationLink(model.users[i]) { MVUserDetailView(model: model, index: i) }
            }
            .navigationTitle("Users (MV)")
        }
    }
}

struct MVUserDetailView: View {
    @ObservedObject var model: UserModel
    let index: Int
    var body: some View {
        VStack {
            Text(model.users[index])
            Button("Rename to Charlie") { model.users[index] = "Charlie" }
        }
        .navigationTitle("Detail")
    }
}

この構成では、UserModel の所有関係がUI階層と一致しており、状態の流れが非常にシンプルになります。 親Viewが @StateObject としてModelを保持し、 状態の共有先ではそれを @Binding、@ObservedObject、@Environment、あるいは@EnvironmentObject として受け取るだけで、双方向の同期が自動的に行われます。

ViewModel を介さないため、状態の伝搬経路が明確で、どのViewがどの状態を所有しているかがコード上で直感的に把握できます。 これにより、画面遷移や状態共有の仕組みを複雑化させず、SwiftUI本来の「データ駆動のUI更新」という思想に沿った設計を保てます。

MVは「スモールスタートしやすいアーキテクチャ」であり、試作段階や画面単位の小規模構成では非常に有効です。 特に、Apple公式サンプル Food Truck の FoodTruckModel は、MVパターンにおける状態管理の例として参考になります。 しかし、画面数や状態が増えるにつれ、次のような課題が生まれやすくなります。

MVアーキテクチャで発生しやすい課題

MVはシンプルな構造で開発しやすい反面、規模が大きくなると次のような課題が生じやすくなります。

Fat View問題

View が 状態管理・イベント処理・描画・Model呼び出し のすべてを担い、肥大化しやすい問題です。 とくに非同期処理や複数のModelを扱うケースでは、 1つのViewが実質的にViewModelのような役割を担ってしまい、 可読性や再利用性が低下します。

struct UserListView: View {
    @StateObject private var model = UserModel()
    @State private var isLoading = false
    @State private var errorMessage: String?

    var body: some View {
        VStack {
            if isLoading {
                ProgressView()
            } else {
                List(model.users, id: \.id) { user in
                    Text(user.name)
                }
            }

            Button("Reload") {
                Task {
                    isLoading = true
                    do {
                        try await model.fetchUsers()
                    } catch {
                        errorMessage = error.localizedDescription
                    }
                    isLoading = false
                }
            }
        }
        .alert("Error", isPresented: .constant(errorMessage != nil)) {
            Button("OK", role: .cancel) { errorMessage = nil }
        }
    }
}

このように、UIとロジックが密結合すると テストが困難になり、状態の変更や再利用も難しくなります。

プレゼンテーションロジックの分離困難

UI層で文字列フォーマットや条件分岐を行うようになると、 View が「データをどう見せるか」まで責任を持つようになります。 結果として、見た目とロジックが密結合し、 テストや再利用が難しくなります。

たとえば、次のように View 内でフォーマット関数を定義してしまうケースです。

struct UserRowView: View {
    let user: User

    var body: some View {
        Text(displayName(for: user))
    }

    // ⚠️ View側でフォーマット処理を持つパターン
    private func displayName(for user: User) -> String {
        user.isPremium ? "⭐️ \(user.name)" : user.name
    }
}

このような構成では、UIの修正とロジックの修正が混ざりやすく、 UserRowView を他画面で再利用することも難しくなります。

責務境界の曖昧化

ViewがUI構築だけでなく状態やロジックも持つようになると、 「どの層が何を担うか」が不明確になり、 チーム内でのコードスタイルや設計方針のブレを生みやすくなります。 結果的に、画面間で同じ処理が重複したり、責務の分離が形骸化してしまいます。

これらの課題を解決するために、MVの思想をベースに 責務を再分離したパターン が検討されるようになりました。次章では、そのアプローチとして採用された Container/Presentationパターン について紹介します。

Container/Presentationパターンの紹介

今回紹介するContainer/Presentationパターンは、画面の責務を明確に分離し、 保守性を高めるための設計アプローチです。具体的には以下のような役割分担を行います。

※補足:本記事での「Container / Presentation」は、 Reactのコンポーネントパターンとは一部異なる意味で使用しています。 SwiftUIの仕組みに合わせて再解釈した独自の構成を含みます。

Presentation View

受け取ったデータをもとに純粋なUIを構築します。状態管理やロジックは持たず、表示の責務に徹します。 テストやプレビューが容易になるのが特徴です。

Container View

画面全体の状態管理やイベントハンドリングを担当します。 各種Modelや画面固有の状態を組み立て、PresentationViewに必要なデータ・アクションを渡します。 MVVMでいうViewModelの責務をContainerViewに寄せるイメージです。

Model

アプリ全体またはドメイン単位で状態やビジネスロジックを管理する層です。 Containerから監視され、必要に応じて状態を変更します。 MV(Container/Presentation)では、Modelが「アプリ全体の真実のソース(Single Source of Truth)」 となり、 Containerはその監視・操作役として機能します。

✅ 補足: Modelは単なるデータ構造ではなく、アプリのビジネスロジックや状態遷移ルールも内包します。 一方で、画面固有の一時的な状態(例:アラートの開閉や入力状態など)は、Container側で保持します。

以下は、ContainerViewで @StateObject と @EnvironmentObject を使い分け、PresentationViewが純粋なUIのみを担う例です。

import SwiftUI

// Model
class UserModel: ObservableObject {
    @Published var name: String = "Alice"
    @Published var isPremium: Bool = true
}

class SettingsModel: ObservableObject {
    @Published var darkModeEnabled: Bool = false
}

// ContainerView
struct UserContainerView: View {
    @StateObject private var userModel = UserModel()
    @EnvironmentObject var settingsModel: SettingsModel

    var body: some View {
        UserView(
            name: userModel.name,
            isPremium: userModel.isPremium,
            darkModeEnabled: $settingsModel.darkModeEnabled,
            onTogglePremium: {
                userModel.isPremium.toggle()
            }
        )
    }
}

// PresentationView
struct UserView: View {
    let name: String
    let isPremium: Bool
    @Binding var darkModeEnabled: Bool
    let onTogglePremium: () -> Void

    var body: some View {
        VStack {
            HStack {
                Text(name)
                if isPremium {
                    Text("⭐️ Premium")
                }
            }
            .onTapGesture {
                onTogglePremium()
            }

            Toggle("Dark Mode", isOn: $darkModeEnabled)
                .padding()
        }
    }
}

この例では、UserModel は必要最低限の状態を @Published で管理し、UserView は完全に受け取り専用の表示コンポーネントとして設計されています。ContainerViewが従来のViewModelのような状態の監視・更新を一元管理することで、責務の分離と保守性を実現しています。

ViewがModelを監視するだけのシンプルなSwiftUIのフレームワークの使い方をしているので、 MVVMの場合に発生するCombineを使用した状態監視の学習コストもかからず、シングルトンを使った考慮も不要です。 Userの状態がUserContainerViewに閉じているため、他の画面でもUserContainerViewの処理を再定義する必要がなく、再利用することが可能になります。

例:UserContainerViewを再利用する例

struct SettingContainerView: View {
  var body : some View {
    VStack {
      UserContainerView() // ユーザ情報に関する機能を持ったContainerViewを別のContainerでも呼び出せる
    }
  }
}

ここでModelとContainerViewとの責務を考えた時にContainerViewが持つべきでないと考える責任は以下になります。

// ❌ APIクライアントの直接呼び出し → Modelに集約する
private func loadUsers() {
    apiClient.fetchUsers { users in
        self.users = users
    }
}

// ❌ 複雑なビジネスロジック → Modelに集約する
private func calculateUserScore(_ user: User) -> Double {
    // 複雑な計算処理はModelの責任
}

// ❌ UIコンポーネントの詳細実装 → PresentationViewに集約する
var body: some View {
    VStack {
        HStack {
            Image(systemName: "person")
            Text(user.name)
        }
    }
}

参考資料

Introducing Container views in SwiftUI | Swift with Majid

Intro To Mv State Pattern | AzamSharp

MV vs MVVM in SwiftUI (2025): Which Architecture Should You Use?

SwiftUI に、Container / Presentational パターンという選択肢 #React - Qiita

GitHub - apple/sample-food-truck: SwiftUI sample code from WWDC22

SwiftUI Architecture - Best Practices and Principles

MV(Container/Presentation)の課題と対応策

MV(Container/Presentation)にも課題があります。運用前から把握していた課題や運用後に気づいた課題など、 以下に課題とそれに対する解決策を紹介します(検討中の解決策も含みます)。

Observabable Objectの問題

MV(Container/Presentation)固有の問題ではないですが、EnvironmentObjectを使用する際は注意して使用する必要があります。 特にObservable Objectの場合は、Observable Objectのプロパティが変化すると、Observable Objectを宣言しているView全体が再描画されるケースがあるので注意です。 ただし、Observable→Observationに移行することで、上記問題がなくなるため、minimum Deployment TagetをiOS17以上に設定できる場合はObservationを検討した方が良いと考えます。

プレゼンテーションロジックの問題

MVVMであればViewModelに書いていたプレゼンテーションロジックをどこに書くかという問題が生じます。 対応策としては、Formattable ProtocolまたはExtensionを定義するという方法が有効です。

// 解決方法:各データモデルに対してExtensionを定義
extension User {
    var displayName: String {
        isPremium ? "⭐️ \(name)" : name
    }
}

// 解決方法:Formattable Protocolで定義
protocol UserFormattable {
    var name: String { get }
    var isPremium: Bool { get }
    var displayName: String { get }
}

extension User: UserFormattable {
    var displayName: String {
        isPremium ? "⭐️ \(name)" : name
    }
}

UserFormattable または User の displayName に対して単体テストを行うことで、プレゼンテーションロジックの妥当性を担保できます。

注:ロジックが軽い場合は上記で十分ですが、ロジックが重い(フォーマットや状態変換が高コスト)場合は次章のパフォーマンス対策を検討します。

プレゼンテーションロジックのパフォーマンスを考慮する場合

高速スクロールが発生するListなどでは、フォーマットやチェック状態の付与を いつ・どこで 行うかが フレーム落ち(ヒッチ)に直結します。ここではA/Bの方針を示します。

A. 各セルごとに非同期でバックグラウンド処理する

表示直前に行単位でフォーマットを行う方法になります。

struct User: Identifiable {
    let id: UUID
    let name: String
    let isPremium: Bool
}

// 表示用フォーマットを非同期で提供する拡張
extension User {
    func formattedDisplayName() async -> String {
        await Task.detached { [name = self.name, isPremium = self.isPremium] in
            isPremium ? "⭐️ \(name)" : name
        }.value
    }
}

// 各セルで非同期にフォーマットを実行
struct UserRowView: View {
    let user: User
    @State private var displayName: String = ""

    var body: some View {
        Text(displayName.isEmpty ? user.name : displayName)
            .task {
                displayName = await user.formattedDisplayName() // 非同期でフォーマット
            }
    }
}

特徴として、動的でメモリ効率は良いですが、セル数や処理コストによってはヒッチが発生しやすくなります。

B. 事前に一時的な表示用Stateを用意してから描画する

事前に一時的な表示用Stateを用意してから描画する方法になります。 ContainerView側で非同期にフォーマット済みデータを構築してから表示する方針になります。

struct User: Identifiable {
    let id: UUID
    let name: String
    let isPremium: Bool
}

/// フォーマット済みの状態
struct UserFormattedState: Identifiable {
    let id: UUID
    let displayName: String
}

@MainActor
final class UsersModel: ObservableObject {
    @Published var users: [User] = []
}

struct UserListContainerView: View {
    @StateObject private var model = UsersModel()
    @State private var displayStates: [UserFormattedState] = []

    var body: some View {
        List(displayStates) { s in
            Text(s.displayName)
        }
        .task { await prepareDisplayStates() }
    }

    private func prepareDisplayStates() async {
        let base = model.users
        let mapped = await withTaskGroup(of: UserFormattedState.self) { group in
            for u in base {
                group.addTask {
                    let name = u.isPremium ? "⭐️ \(u.name)" : u.name
                    return UserFormattedState(id: u.id, displayName: name)
                }
            }
            return await group.reduce(into: []) { $0.append($1) }
        }
        displayStates = mapped
    }
}

Bの方針では、モデル変更時に差分更新の頻度を制御しやすく、描画負荷の予測可能性が高まります。 Aの方針で進めつつ、高速スクロールが求められる場合やフォーマット処理に時間がかかる場合はBの方針を検討するのが良いかと思います。

ContainerViewで定義するプロパティ状態が増える点

AlertやNavigation状態のプロパティがContainerViewごとに増え、ボイラープレート化する場合があります。 対応策としては、ライフサイクルが同じプロパティは AlertState や NavigationState などの構造体として切り出す方法が有効です。

struct AlertState {
    var isPresented = false
    var title = ""
    var message = ""
}

struct UserContainerView: View {
    @State private var alertState = AlertState()
    var body: some View {
        ...
    }
}

画面固有の一時的な状態をひとまとめにしたい場合の命名について

ContainerViewで扱うプロパティが増えると、 アラートやチェック状態のような「画面固有の一時的な状態」をひとまとめにしたい場面が出てきます。その際、まとめた構造体を Model と呼ぶべきか、State と呼ぶべきか という名前の問題が生じます。

本記事では以下のように整理しました。

  • Model:ビジネスロジックを含むアプリケーションの状態(例:UserModel、単体テストの対象となるロジックを含む)
  • State:ビジネスロジックを含まない、画面固有の一時的な状態のまとまり(例:AlertState, UserCheckedState, NavigationState)

StateはViewModelと似ていますが、 あくまで「特定のUI操作に必要な状態」だけを切り出した より小さな粒度 の概念であり、 再利用しやすい点も特徴です。

※ Apple公式のViewModelとの名称のずれについて

Appleの WWDC「InstrumentsによるSwiftUIのパフォーマンス最適化」では、 お気に入りボタンのように View に紐づく“個別の状態のまとまり”を ViewModel と呼んでいる例があります。

ただし、ここでいう ViewModel は 一般的なMVVMの「画面全体の状態管理を行うViewModel」とは役割が異なります。

SwiftUIでは View が階層構造を持ち、自身が必要とする状態だけを 小さく構造体としてまとめることが自然であるため、 Appleが「ViewModel」と呼ぶ例は “View専用の局所的な状態の束ね役” というニュアンスに近いものです。

そのため、本記事では混乱を避けるためにViewModelという名称は使用せず、あくまで

  • Model = アプリケーション全体の状態/ビジネスロジック
  • State = 画面固有の一時的な状態
  • Container = それらを組み合わせてUIに渡す層

という名称で統一しています。

youtu.be

ViewとContainerViewの責務が曖昧になる問題

UIの詳細はPresentationViewの責務ですが、 小規模な画面では「ちょっとした処理ならContainerに書いてもいいか」となりがちです。 しかし、それが積み重なると、ContainerViewがUIロジックを抱え込み、 結果として Containerが肥大化してPresentationとの境界が曖昧になる という問題が発生します。

この問題に対する対応策としては、責務を明確に保つために 層の依存方向を強制的に制御する ことが有効です。 具体的にはPresentationViewを別モジュールに分割するという方法になります。

Presentation側からModel層へ直接アクセスできないようにすることで、 Container経由でしか状態を変更できない構造を強制できます。 結果として、「UIはUIに専念する」という責務分離をコードレベルで保証できます。 この時依存関係はContainerViewモジュール→PresentationViewモジュールという方向性になります。

副次的なメリットとして、Presentationモジュールを独立させることで依存関係が軽くなり、 SwiftUI Previewsのビルド時間も短縮されるという効果もあります。

// アプリケーション本体のModule

class UserModel: ObservableObject {
   // 省略
}

struct UserContainerView: View {
    @StateObject var userModel = UserModel()
    var body: some View {
        UserView(name: userModel.name, isPremium: userModel.isPremium) // Viewは呼び出すだけ
     .sheet(isPresented: ... )
           .alert(isPresented: ... )
    }
}

// ---------------------------

// PresentationModule
// アプリケーション本体には依存させない

struct UserView: View {
    let name: String
    let isPremium: Bool
    var body: some View {
        Text(isPremium ? "⭐️ \(name)" : name)
    }
}

モジュール分割に際して、PresentationViewにContainerViewが入り込む問題

上記のモジュール分割の副作用として、PresentationView内に特定のContainerViewが含まれる構造になると、PresentationViewの再利用や定義が難しくなります。

// PresentationModule

struct ProductCardView: View {
    let productName: String

    var body: some View {
        VStack {
            Text(productName)
            FavoriteContainerButton() // ContainerViewはアプリケーション本体モジュールで定義されているため利用できない
        }
    }
}

対応策としては以下の2つがあります。

Aパターン: ContainerViewを直接含めず、PresentationViewをラップするContainerViewで外部から注入する。

struct ProductCardView: View {
    let productName: String
    let onTapFavorite: () -> Void
    var body: some View {
        VStack {
            Text(productName)
            FavoriteButton { // FavoriteのUIのみ定義
                onTapFavorite() // 処理は外部から入れ込む
            }
        }
    }
}

// 処理をContainerView側で定義
struct ProductCardContainerView: View {
    var body: some View {
        ProductCardView(productName: "Sample Product") {
            // Favoriteボタン押下時の処理
        }
    }
}

Bパターン: @ViewBuilder を使用して、外部からContainerViewを注入できるようにする。

struct ProductCardView<ActionButtons: View>: View {
    let productName: String
    let actionButtons: ActionButtons

    init(productName: String, @ViewBuilder actionButtons: () -> ActionButtons) {
        self.productName = productName
        self.actionButtons = actionButtons()
    }

    var body: some View {
        VStack {
            Text(productName)
            actionButtons
        }
    }
}

// Container側でFavoriteContainerButtonを注入
struct ProductCardContainerView: View {
    var body: some View {
        ProductCardView(productName: "Sample Product") {
            FavoriteContainerButton()
        }
    }
}

AパターンとBパターンを組み合わせて実装することで、依存方向を保ちながら柔軟な設計が可能になります。

(補足)DDDの概念で捉えた場合の位置づけ

なお、Container/PresentationパターンはDDD(ドメイン駆動設計)を前提とした設計パターンではありませんが、 DDDの用語で理解したいという声もあったため、参考としてゆるく対応づけた整理を紹介します。

Entity + UseCase → Model

アプリケーション状態やビジネスロジックは Model(ObservableObject) に集約します。 ContainerViewが監視し、PresentationViewへと必要な情報を橋渡しします。

@MainActor
final class UsersModel: ObservableObject {
    @Published var users: [User] = []
    func load() async { … }
}

Presenter(UIロジック)→ 値オブジェクトのExtension or Protocol

表示のための加工(Presenter的な責務)は Modelではなく値オブジェクト側の Extension / Protocol で担います。

extension User {
    var displayName: String {
        isPremium ? "⭐️ \(name)" : name
    }
}

ContainerViewはViewのため、単体テストがしづらいため、 画面で必要なフォーマットは「ContainerViewではなく、値オブジェクト側に寄せる」のが基本方針になります。

ViewModel(アプリケーション層)→ ContainerView

MVVMにおけるViewModelは、 Container/Presentationパターンでは ContainerView(+パフォーマンスやプロパティをまとめる際に応じて小さな XXXState)が担います。

struct UserListContainerView: View {
    @StateObject private var model = UsersModel()
    @State private var alertState = AlertState()
}

「画面の状態」と「イベントハンドリング」をContainerViewに集約する立ち位置です。

View

UI描画を行うPresentationViewは、DDDでいうViewとほぼ同じイメージで、 状態もロジックも持たず、描画だけに集中 します。

対応関係まとめ

DDDの概念 MV(Container/Presentation)での対応要素
Entity Model(ObservableObject)
Usecase Model(ObservableObject)のメソッド
Presenter 値オブジェクトのExtension等 or Protocol
ViewModel ContainerView + 必要に応じて XXXState
View PresentationView

まとめ

本記事では、MVVMの課題を回避しつつ、責務分離と保守性を両立する手法として MV(Container/Presentation)パターン を紹介しました。

このアーキテクチャは、SwiftUIの「宣言的UI」という特性と非常に相性が良く、 ロジックと描画の分離によってコードの見通しを大きく改善できます。 一方で、実際の運用では責務境界の管理や状態の扱いなど、新たな設計上の課題も存在します。

アーキテクチャに「唯一の正解」はありません。 プロジェクトの規模、チーム構成、メンバーの習熟度に応じて最適解は変化します。 本記事の内容が、その検討を進める上での一つの指針となれば幸いです。

テコテックの採用活動について

テコテックでは新卒採用、中途採用共に積極的に募集をしています。 採用サイトにて会社の雰囲気や福利厚生、募集内容をご確認いただけます。 ご興味を持っていただけましたら是非ご覧ください。 tecotec.co.jp