NavigationSample 設計情報

設計原則

状態駆動

S1ナビゲーションは「画面遷移」ではなく「状態遷移」である
SwiftUI は命令的に画面を遷移させる UI フレームワークではない。「今どの画面が表示されているか」は、状態の結果として決まる。 つまり開発者が行うのは「画面 A から画面 B に遷移する」ではなく、「状態を変更し、その結果として画面が変わる」ということである。この原則を守ることで、ナビゲーション状態の一元管理・復元・テストが容易になる。
コード例

状態を変更するだけで画面が変わる

// 状態を変更するだけで画面が変わる
path.append(.detail(id))  // push
modal = .likeSend         // modal 表示
適用される画面パターン
全画面共通
S2Path は「画面」ではなく「意味」を表す
状態は「何が起きているか」を直接表現すべきである。Bool は「起きている/いない」しか表せないが、Path は「何が起きているか」を明確に表せる。 Path の命名は画面名ではなくドメイン上の意味を反映させる。これにより、Path を見ただけで「何のために遷移したか」が分かるようになる。
コード例

Path は画面名ではなく意味を表す

// ❌ 画面名
case detailView

// ⭕ 意味
case itemDetail(id: Item.ID)
適用される画面パターン
全画面共通View 実装

文脈構造

C1表示されている View が属するナビゲーション文脈は常に1つである
NavigationStack、Modal(sheet / fullScreenCover)、Tab ——これらは同時に1つの文脈だけが有効になる。 構造的に NavigationStack が複数存在してもよいが、同時に有効なものは1つだけである。この原則を意識することで、ナビゲーション文脈の衝突や予期しない振る舞いを防げる。 文脈が切り替わると、元の文脈は一時停止される。NavigationStack の path は保持されるが操作対象ではなくなる。dismiss / pop により元の文脈が再開される。この仕組みを理解することで、Modal 表示中に背後の NavigationStack を操作してしまうようなバグを防げる。
コード例

各タブが独立した NavigationStack を持つ例

TabView {
    // 各タブが独自の NavigationStack を持つ
    HomeRootView()      // 内部に NavigationStack
    SettingsRootView()  // 内部に NavigationStack
}
// タブ切り替え時、選択されていないタブの
// NavigationStack は「存在するが非アクティブ」
適用される画面パターン
NavigationStack (push)Modal 表示
C2文脈(Context)には階層とスコープがある
文脈は以下の階層で構成される: ・App 全体の文脈 ・Feature の文脈 ・Feature 内フローの文脈 遷移設計とは、どの文脈に切り替わるかを定義することである。状態のスコープは、その意味が通用する範囲に一致すべきであり、上位の文脈で管理すべき状態を下位に持たせてはならない。
適用される画面パターン
NavigationStack (push)Modal 表示
適用画面
ユーザ詳細

Feature境界

F1NavigationStack(push)は同一 Feature 内に限定する
push は文脈の継続を意味する。Feature を跨ぐ push は文脈破壊であり、ナビゲーション状態の管理を複雑にする。 Feature を跨ぐ場合は push ではなく、Modal や Tab 切り替えなどの「文脈の切断」を用いる。 この原則を型レベルで強制するため、Path は Feature 境界を越えない。Feature ごとに Path を定義し、グローバル Path は最小限にとどめる。Path が Feature 境界を越えると、Feature 間の結合度が高くなり、独立した開発やテストが困難になる。
コード例

Feature 単位で Path を定義する例

// Feature 単位で Path を定義
enum HomePath: Hashable {
    case itemDetail(Item.ID)
}

enum SettingsPath: Hashable {
    case detail(String)
}
適用される画面パターン
NavigationStack (push)Feature 内遷移
F2Feature を跨ぐ遷移は「文脈の切断」として扱う
Feature を跨ぐ遷移の手段は以下に限定される: ・Tab 切り替え ・Modal 表示 これらはいずれも「現在の文脈を一時停止し、新しい文脈を開始する」という意味を持つ。 push は文脈の継続を意味するため、Feature 境界を跨ぐ push は一律に禁止する。push 風の UX が必要な場合は fullScreenModal + カスタムトランジションで実現する(手段8)。
適用される画面パターン
Feature 間遷移
適用画面
設定

状態分離

P1Push 用の状態と Modal 用の状態は分離する
push はスタック型([Path])、Modal は排他的(Modal?)であり、同一 state に混在させてはならない。 これらを分離することで、push と Modal のライフサイクルが互いに干渉せず、状態管理がシンプルになる。 分離の根拠として、Modal は「一時的 UI」ではなく「独立した文脈」であることが挙げられる。Modal は dismiss により文脈復帰が起きる独立した文脈であり、内部に独自の Navigation を持つことができる。Modal enum は「文脈のスコープ」で定義する(App 文脈 → MainTabModal、Feature 文脈 → FeatureModal)。画面単位では定義しない。
コード例

RootView の @State で push と modal の状態を分離する例

struct FeatureRootView: View {
    @State private var path: [FeaturePath] = []   // push 用
    @State private var modal: FeatureModal?        // modal 用
}
適用される画面パターン
NavigationStack (push)Modal 表示RootView 設計

責務分離

R1View は遷移の決定権を持たない
View は「意図」を表明するだけであり、遷移の解釈は上位レイヤーの責務である。 View が直接 NavigationLink の遷移先を決定したり、Modal を表示したりするのではなく、RootView から渡されたクロージャを通じて「こうしたい」という意図を伝える。 RootView は @State で path / modal を管理し、子 View にはクロージャで遷移トリガーを渡す。子 View はクロージャを呼ぶだけで、遷移の実体(path.append や modal の変更)を知らない。Feature に依存しない View は Feature 外の共有ディレクトリに配置し、コールバックで意図を上位に委譲する。
コード例

View はクロージャで意図を表明し、RootView が遷移を決定する

// View はクロージャで意図を表明
struct UserDetailView: View {
    var onShowDetail: ((Item.ID) -> Void)?

    Button("詳細を見る") {
        onShowDetail?(id)  // 「詳細を見たい」という意図
    }
}
// RootView が遷移方法を決定

Feature 固有 View と再利用可能 View の対比

// Feature 固有 View: クロージャで遷移トリガーを受け取る
struct UserDetailView: View {
    var onShowPhotoList: (() -> Void)?
    var onShowLikeSend: (() -> Void)?
    // RootView からクロージャ経由で遷移を委譲される
}

// 再利用可能 View: コールバックで意図を委譲する
struct UserPhotoListView: View {
    var onSelectPhoto: (Photo.ID) -> Void
    // Feature に依存しないため、他の Feature でも使える
}
適用される画面パターン
View 実装
R2文脈を開始した主体が、文脈を終了させる責務を持つ
・Feature が開始した文脈は Feature が閉じる ・App が開始した文脈は App が閉じる ・push された画面は、同じ NavigationStack が閉じる ・Modal は、それを管理している状態が閉じる @Environment(\.dismiss) は「文脈を終了したい」という意図の表明として使用可能。SwiftUI が文脈に応じて適切な方法(NavigationStack 内では pop、Modal では dismiss)を決定する。
コード例

Modal を開いた状態を nil に戻して閉じる

// Modal を開いた state(Modal?)を nil に戻す
func dismissModal() {
    modal = nil
}
適用される画面パターン
NavigationStack (push)Modal 表示RootView 設計

結果伝達

E1文脈の終了には結果が伴い、イベントとして上位に伝達する
pop / dismiss は UI 命令ではなく、現在の文脈が終了したという状態遷移の結果である。 文脈の終了には、成功・キャンセル・失敗などドメイン上の意味ある結果が伴う。この結果は「閉じる命令」ではなく「イベント」として上位レイヤーに伝達する。 ModalView は処理結果を Result として上位に返し、上位が結果を解釈して Modal を閉じる。Feature 間の連携も Event として上位に委譲する。これにより Feature は他の Feature の存在を知らずに済み、疎結合なアーキテクチャが実現できる。
コード例

Event を上位に委譲して Feature 間を連携する例

enum SettingsEvent {
    case showProfilePreview
}

func handle(_ event: SettingsEvent) {
    switch event {
    case .showProfilePreview:
        currentModal = .profilePreview
    }
}
適用される画面パターン
Modal 表示Feature 間遷移

具体的手段

手段1Feature 単位で Path を定義する
Path は Feature の境界内で定義する。Feature ごとに専用の Path enum を持つことで、Feature 間の結合度を下げ、独立した開発・テストを可能にする。 グローバルな Path は最小限にとどめ、各 Feature が自身の遷移先を型安全に管理する。
コード例

Feature 単位で Path を定義する例

enum HomePath: Hashable {
    case itemDetail(Item.ID)
}

enum SettingsPath: Hashable {
    case detail(String)
}
関連する設計原則
F1: Push限定原則
手段2NavigationStack は Feature の Root にのみ置く
NavigationStack は Feature の RootView にのみ配置し、子 View には配置しない。 これにより、Feature 内のナビゲーション文脈が1つに保たれ、文脈の衝突を防げる。また、path の管理が RootView に集約されるため、状態の追跡が容易になる。
コード例

Feature の RootView に NavigationStack を配置する例

struct HomeRootView: View {
    @State private var path: [HomePath] = []

    var body: some View {
        NavigationStack(path: $path) {
            HomeView()
        }
    }
}
手段3Modal は Path とは別の enum として定義する
push 用の Path と Modal は別の enum として定義する。 push はスタック型([Path])、Modal は排他的(Modal?)であり、性質が異なるため同一の enum に混在させてはならない。Modal 用の enum は Identifiable に準拠させ、item: ベースの sheet/fullScreenCover で使用する。
コード例

Modal を Identifiable な enum で定義する例

enum MainTabModal: Identifiable {
    case profilePreview
    case web(URL)

    var id: String {
        switch self {
        case .profilePreview: return "profilePreview"
        case .web(let url): return "web-\(url.absoluteString)"
        }
    }
}
手段4Modal は item: ベースで制御する
Modal の表示制御には isPresented: ではなく item: を使う。 item: ベースでは「何が表示されているか」をPath / Modal enum で明示的に表現でき、意味表現原則に合致する。isPresented: + Bool では「表示中かどうか」しか表せず、複数の Modal を管理する際に状態が複雑になる。
コード例

item: ベースで Modal を制御する例

.sheet(item: $modal) { modal in
    switch modal {
    case .likeSend:
        LikeSendRootView(
            user: viewModel.user,
            onEvent: { event in /* ... */ }
        )
    }
}
関連する設計原則
P1: Push/Modal分離原則
手段5Modal は Feature として RootView を持たせる
Modal で表示する画面にも独自の RootView と NavigationStack を持たせ、Feature として設計する。 Modal は独立した文脈であり(P2)、内部に独自のナビゲーションフローを持てる。RootView を Feature エントリポイントとすることで、Modal 内の遷移も型安全に管理できる。
コード例

Modal 用の Feature RootView の例

struct OnboardingRootView: View {
    @State private var path: [OnboardingPath] = []

    var body: some View {
        NavigationStack(path: $path) {
            OnboardingStartView()
        }
    }
}
手段6Feature 間遷移は Event として上位に委譲する
Feature が他の Feature への遷移を必要とする場合、直接遷移するのではなく Event として上位レイヤーに委譲する。 Feature は自身の Event enum を定義し、上位(Coordinator / App 層)がその Event を解釈して適切な遷移を実行する。これにより Feature 間の疎結合を維持できる。
コード例

Event を上位に委譲する例

enum SettingsEvent {
    case showProfilePreview
}

func handle(_ event: SettingsEvent) {
    switch event {
    case .showProfilePreview:
        currentModal = .profilePreview
    }
}
手段7遷移を指示するコードは状態を書き換えるだけにする
遷移を発生させるコードは、状態の書き換えのみを行う。命令的な画面遷移メソッドの呼び出しは行わない。 遷移の意味と対応するコード: ・Feature 内 push → RootView の @State を変更 → path.append(destination) ・Feature 内 pop → RootView の @State を変更 → path.removeLast() ・Feature 内 modal → RootView の @State を変更 → modal = .xxx ・modal dismiss → RootView の @State を変更 → modal = nil ・Feature 跨ぎ → onEvent クロージャで上位に委譲 → onEvent(.xxx) ・App modal(SwiftUI) → appModal = .xxx — App 層で実行 ・App modal(UIKit) → present(hostingController, animated:) — Coordinator で実行 ・modal dismiss(UIKit) → dismiss(animated:) — Coordinator で実行 ・文脈終了の意図表明 → onEvent クロージャで上位に委譲 → onEvent(.closeRequested)
関連する設計原則
S1: 状態結果原則
手段8表示手段(トランジション・インタラクティブ dismiss)は Coordinator の責務とする
同一の Feature でも表示元によって表示手段が異なることがある。 例: ・UserGrid → UserDetail: push 風カスタムトランジション + スワイプ dismiss ・Settings → プロフィールプレビュー: 通常 fullScreenModal(スワイプ dismiss なし) Feature の API に表示手段に依存するパラメータ(ナビゲーション深度コールバック等)を追加しない。表示手段の違いは Coordinator が吸収する。スワイプ dismiss の制御には Environment + ViewModifier を使う。SwipeDismissalInteractor を .environment で注入し、.swipeDismissable(isAtRoot:) ViewModifier で「ナビゲーションルートかどうか」を宣言的に制御する。
コード例

Environment + ViewModifier でスワイプ dismiss を宣言的に制御する例

// Coordinator が SwipeDismissalInteractor を生成し Environment で注入
func showUserDetail(user: User) {
    let dismissalInteractor = SwipeDismissalInteractor()

    let detailRootView = UserDetailRootView(
        viewModel: UserDetailViewModel(user: user),
        onEvent: { [weak self] event in self?.handle(event) }
    )
    .environment(\.swipeDismissalInteractor, dismissalInteractor)

    // ...
}

// Feature 内では .swipeDismissable で宣言的に制御
NavigationStack(path: $path) {
    UserDetailView(/* ... */)
}
.swipeDismissable(isAtRoot: path.isEmpty)
手段9push 用 path は型安全な [Path] を使用する
push 用の path は原則として [Path] を使用する。NavigationPath は例外的なケースのみ検討する。 [Path] と NavigationPath の比較: ・型安全性 — [Path] はコンパイル時チェック、NavigationPath はランタイムのみ ・Feature 境界の強制 — [Path] は型エラーで防止、NavigationPath は防止不可 ・状態復元 — [Path] は Codable で直接対応、NavigationPath は CodableRepresentation 経由 ・網羅性チェック — [Path] は switch で強制、NavigationPath は不可 ・複数型の混在 — [Path] は単一型に限定、NavigationPath は可能 NavigationPath を検討するケース(例外的): ・App 層での統合ナビゲーション(オンボーディングフロー等) ・CMS 連携等の動的な画面構成 ・外部 URL からの複雑な Deep Linking 復元
コード例

推奨: 型安全な [Path]

// ✅ 推奨: 型安全な [Path]
@State private var path: [HomePath] = []

非推奨: 型消去された NavigationPath

// ❌ 使用しない: 型消去された NavigationPath
@State private var path = NavigationPath()
関連する設計原則
F1: Push限定原則
手段10UIKit App 層 + SwiftUI Feature 層で構成する
UIKit ベースの既存アプリに SwiftUI を導入する際の推奨構成。 構成: UIKit App ├── AppDelegate.swift (UIKit) ├── SceneDelegate.swift (UIKit) ├── MainTab/ │ ├── MainTabCoordinator.swift (UIKit Coordinator) │ └── MainTabBarController.swift (UITabBarController) └── Features/ ├── Home/ (UIHostingController + SwiftUI NavigationStack) └── Settings/ (UIHostingController + SwiftUI NavigationStack) UIKit Coordinator が App 層の Modal 表示・非表示を管理し、Feature 層は Event を上位に委譲するのみ。UITabBarController の各タブが UIHostingController で SwiftUI View をラップし、タブ切り替え時、選択されていないタブの NavigationStack は非アクティブになる。 ポイント: ・SwiftUI View は既存の設計を変更不要(onEvent パターンはそのまま使用可能) ・UIKit Coordinator が App 層の Modal 状態・Tab 選択状態を管理 ・Feature の RootView が @State で Feature 内の path・modal 状態を管理 ・イベントフロー: SwiftUI View → Event → UIHostingController → Coordinator → UIKit の遷移処理
コード例

MainTabCoordinator(UIKit)の例

@MainActor
final class MainTabCoordinator {
    private let window: UIWindow
    private var tabBarController: MainTabBarController?
    var currentModal: MainTabModal?

    func start() {
        let tabBarController = MainTabBarController(coordinator: self)
        self.tabBarController = tabBarController
        window.rootViewController = tabBarController
        window.makeKeyAndVisible()
    }

    func handle(_ event: SettingsEvent) {
        switch event {
        case .showProfilePreview:
            presentProfilePreview()
        case .openHome:
            tabBarController?.selectedIndex = 0
        }
    }
}

MainTabBarController(UIKit)の例

final class MainTabBarController: UITabBarController {
    private weak var coordinator: MainTabCoordinator?

    private func setupTabs() {
        // Home Tab (UIKit ベースのグリッド)
        let homeNavController = UINavigationController()
        homeNavController.tabBarItem = UITabBarItem(
            title: "ホーム",
            image: UIImage(systemName: "heart.circle"),
            tag: 0
        )
        coordinator?.setupUserGridCoordinator(
            navigationController: homeNavController
        )

        // Settings Tab (SwiftUI ベース)
        let settingsRootView = SettingsRootView(onEvent: { [weak self] event in
            self?.coordinator?.handle(event)
        })
        let settingsVC = UIHostingController(rootView: settingsRootView)
        settingsVC.tabBarItem = UITabBarItem(
            title: "設定",
            image: UIImage(systemName: "gear"),
            tag: 1
        )

        viewControllers = [homeNavController, settingsVC]
    }
}
手段11UIKit / SwiftUI 連携パターンを用途に応じて使い分ける
UIKit App 層と SwiftUI Feature 層の境界では、用途に応じて以下のパターンを使い分ける。 パターン A: SwiftUI Feature 内で UIKit 画面を modal 表示 ・既存 UIKit 画面の遷移ロジックを変更せずに統合できる ・UINavigationController でラップして modal 表示 ・閉じるときだけ SwiftUI 側に onDismiss で通知 パターン B: UIKit 画面から SwiftUI Feature を modal 表示 ・UIHostingController で SwiftUI View をラップして present ・onEvent クロージャで UIKit 側にイベントを伝達 ・push 風の UX が必要な場合は fullScreenModal + カスタムトランジションで実現する(手段8) パターン C: UIKit 画面の一部を SwiftUI で構築 ・UIHostingController を child view controller として追加 ・Auto Layout で SwiftUI View のサイズ・位置を制御 ・SwiftUI View からのイベントはクロージャで UIKit 側に伝達
コード例

パターン A: SwiftUI Feature 内で UIKit 画面を modal 表示

// UINavigationController を SwiftUI でラップ
struct LegacyProfileNavigationControllerRepresentable:
    UIViewControllerRepresentable
{
    let rootViewController: UIViewController

    func makeUIViewController(context: Context) -> UINavigationController {
        UINavigationController(rootViewController: rootViewController)
    }

    func updateUIViewController(
        _ uiViewController: UINavigationController,
        context: Context
    ) {}
}

// sheet で表示
.sheet(item: $modal) { modal in
    switch modal {
    case .legacyProfile:
        LegacyProfileModalView(
            user: viewModel.user,
            onDismiss: { self.modal = nil }
        )
    }
}

パターン B: UIKit 画面から SwiftUI Feature を modal 表示

final class SomeViewController: UIViewController {
    private func presentSwiftUIFeature() {
        let swiftUIView = SettingsRootView(onEvent: { [weak self] event in
            self?.handleSettingsEvent(event)
        })
        let hostingController = UIHostingController(rootView: swiftUIView)
        hostingController.modalPresentationStyle = .fullScreen
        present(hostingController, animated: true)
    }
}

パターン C: UIKit 画面の一部を SwiftUI で構築

final class HybridViewController: UIViewController {
    private var hostingController: UIHostingController<SomeSwiftUIView>?

    private func setupSwiftUIView() {
        let swiftUIView = SomeSwiftUIView(
            onTap: { [weak self] in self?.handleTap() }
        )
        let hosting = UIHostingController(rootView: swiftUIView)

        addChild(hosting)
        view.addSubview(hosting.view)
        hosting.didMove(toParent: self)

        hosting.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hosting.view.topAnchor.constraint(
                equalTo: view.safeAreaLayoutGuide.topAnchor
            ),
            hosting.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hosting.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hosting.view.heightAnchor.constraint(equalToConstant: 200),
        ])

        hostingController = hosting
    }
}
手段12RootView で @State を管理し、子 View にはクロージャで遷移トリガーを渡す
RootView が @State で path / modal を直接管理し、子 View にはクロージャで遷移トリガーを渡す。子 View はクロージャを呼ぶだけで、遷移の実体を知らない。 Environment は SwipeDismissalInteractor のようにView 階層を跨いで共有するサービス的な依存の伝搬に使う。遷移トリガーのように「何をしたいか」を伝える用途にはクロージャの方が依存関係が明確で適切。 一方、ViewModel のように特定の View だけが消費する1対1 の依存は init 引数で直接渡す。依存の共有範囲に応じて使い分ける。
コード例

RootView で @State を管理しクロージャで子 View に渡す例

struct UserDetailRootView: View {
    @State private var path: [UserDetailPath] = []
    @State private var modal: UserDetailModal?
    @State private var viewModel: UserDetailViewModel

    var body: some View {
        NavigationStack(path: $path) {
            UserDetailView(
                viewModel: viewModel,
                onShowPhotoList: {
                    path.append(.photoList(viewModel.user.id))
                },
                onShowLikeSend: {
                    modal = .likeSend
                }
            )
        }
    }
}

子 View がクロージャで遷移トリガーを受け取る例

struct UserDetailView: View {
    let viewModel: UserDetailViewModel
    var onShowPhotoList: (() -> Void)?
    var onShowLikeSend: (() -> Void)?

    var body: some View {
        Button("写真一覧") { onShowPhotoList?() }
        Button("いいね!") { onShowLikeSend?() }
    }
}
関連する設計原則
C2: 階層スコープ原則
手段13再利用可能な View はコンポーネント化して Feature から切り離す
View は Feature 固有 View と再利用可能 View に分類できる。 Feature 固有 View は特定の Feature でのみ使用されるため、Features/XXX/Views/ に配置し、クロージャで遷移トリガーを受け取る。一方、再利用可能 View は複数の Feature で使用される可能性があるため、Feature のドメインモデルに強く結合してはならない。Feature に依存しない View は Shared/Components/ 等のFeature 外ディレクトリに配置する。 判断基準: その View が Feature のドメインモデルに強く結合することで、不自然な依存が生まれるなら、その View は再利用可能 View としてコールバックで委譲すべきである。 再利用可能 View はコールバック(クロージャ)で「意図」を上位に伝え、RootView の navigationDestination 内でコールバックと @State の変更を接続する。 ディレクトリ構成例: Features/UserProfile/Views/ … Feature 固有 View(クロージャ委譲) Shared/Components/ … 再利用可能 View(コールバック委譲) Shared/Models/ … 共有モデル
コード例

再利用可能 View: コールバックで意図を委譲する

struct UserPhotoListView: View {
    let photos: [Photo]
    var onSelectPhoto: (Photo.ID) -> Void

    var body: some View {
        List(photos) { photo in
            Button {
                onSelectPhoto(photo.id)
            } label: {
                PhotoRow(photo: photo)
            }
        }
    }
}

RootView でコールバックと @State を接続する

.navigationDestination(for: UserDetailPath.self) { destination in
    switch destination {
    case .photoList(let userId):
        UserPhotoListView(
            photos: viewModel.photos,
            onSelectPhoto: { photoId in
                // コールバックを受けて @State で遷移
                path.append(.photoDetail(photoId))
            }
        )
    case .photoDetail(let photoId):
        UserPhotoDetailView(photoId: photoId)
    }
}
関連する設計原則
R1: View無決定権原則

画面パターン別マトリクス

各画面パターンに適用される設計原則の一覧です。

パターンS1S2C1C2F1F2P1R1R2E1
全画面共通
NavigationStack (push)
Modal 表示
Feature 内遷移
Feature 間遷移
View 実装
RootView 設計

画面設計情報

ユーザグリッド (Home)
フレームワークUIKitレイヤーFeature 層RootViewUserGridViewController
UIKit の UICollectionView でユーザ一覧をグリッド表示。 各セルは UIHostingConfiguration を使い SwiftUI で実装している。 セル選択時に UserGridCoordinator を介して SwiftUI の UserDetail Feature を fullScreenModal + push 風カスタムトランジションで表示する(パターン B + 手段8)。
実装パターン
パターン B: UIKit 画面から SwiftUI Feature を modal 表示パターン C: UIKit 画面の一部を SwiftUI で構築
ユーザ詳細
フレームワークSwiftUIレイヤーViewRootViewUserDetailRootView
UserDetail Feature の起点画面。NavigationStack の root として機能し、 Feature 内で push 遷移(写真一覧・写真詳細)と modal 表示(いいね送信画面)を管理する。 path / modal を @State で直接管理し、子 View にはクロージャで遷移トリガーを渡す。 DisplayMode による表示パターンの違い: ・standard — Home タブからの遷移。左上に「← 戻る」ボタン、「いいね!」ボタンあり。 ・me — 設定タブからのプロフィールプレビュー。右上に「×」ボタン(iOS 慣例の閉じるボタン配置)、「いいね!」ボタンなし。 どちらも onEvent(.closeRequested) で App 層に終了を通知する点は共通(R2, E1)。
実装パターン
Feature Root (NavigationStack)Feature 内 pushFeature 内 modal
写真一覧
フレームワークSwiftUIレイヤーViewRootViewUserPhotoListView
UserDetail Feature 内で push 遷移で表示される写真一覧画面。 写真タップで写真詳細画面にさらに push 遷移する。 クロージャを通じて遷移を要求し、View 自身は遷移を決定しない(R1)。
実装パターン
Feature 内 push
写真詳細
フレームワークSwiftUIレイヤーViewRootViewUserPhotoDetailView
UserDetail Feature 内で push 遷移で表示される写真詳細画面。 Feature 内の最深部の画面。戻るボタンで NavigationStack が pop を処理する。
実装パターン
Feature 内 push
いいね送信
フレームワークUIKitレイヤーFeature 層RootViewLikeSendRootView
独立した LikeSend Feature。UserDetail Feature からセミモーダルで表示される。 LikeSendRootView が Feature エントリポイントで、UIKit の LikeSendViewController を UIViewControllerRepresentable でラップ(パターン A)。 LikeSendEvent で上位にいいね送信完了・キャンセルを通知する。
実装パターン
パターン A: SwiftUI Feature 内で UIKit 画面を modal 表示
設定
フレームワークSwiftUIレイヤーViewRootViewSettingsRootView
Settings Feature の起点画面。Feature 内に push 遷移を持たず、 Event 委譲のみで App 層と連携する最小構成の Feature。 「プロフィールプレビュー」は Event 経由で App 層に fullScreenModal 表示を要求(E2)。 同一の UserDetail Feature を Home とは異なる表示手段で表示する(手段8)。 「マッチするにはいいねしよう!」はタブ切り替えを Event で委譲する(E2, F2)。
実装パターン
Feature Root (NavigationStack)
設計情報
フレームワークSwiftUIレイヤーViewRootViewDesignOverviewView
設計原則・具体的手段・画面パターン別マトリクスを俯瞰する画面。 独自の NavigationStack を持ち、原則詳細・手段詳細への push 遷移を提供する。
実装パターン
Feature Root (NavigationStack)