axiom-app-composition

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

App Composition

应用组合

When to Use This Skill

何时使用该技能

Use this skill when:
  • Structuring your @main entry point and root view
  • Managing authentication state (login → onboarding → main)
  • Switching between app-level states without flicker
  • Handling scene lifecycle events (scenePhase)
  • Restoring app state after termination
  • Deciding when to split into feature modules
  • Coordinating between multiple windows (iPad, axiom-visionOS)
在以下场景使用本技能:
  • 构建@main入口点与根视图
  • 管理认证状态(登录 → 引导页 → 主应用)
  • 无闪烁地切换应用级状态
  • 处理场景生命周期事件(scenePhase)
  • 应用终止后恢复状态
  • 决定何时拆分为功能模块
  • 协调多窗口(iPad、visionOS)

Example Prompts

示例提问

What You Might AskWhy This Skill Helps
"How do I switch between login and main screens?"AppStateController pattern with validated transitions
"My app flickers when switching from splash to main"Flicker prevention with animation coordination
"Where should auth state live?"App-level state machine, not scattered booleans
"How do I handle app going to background?"scenePhase lifecycle patterns
"When should I split my app into modules?"Decision tree based on codebase size and team
"How do I restore state after app is killed?"SceneStorage and state validation patterns
你可能的提问本技能的作用
"如何在登录页和主屏幕之间切换?"基于AppStateController模式实现经过验证的状态过渡
"我的应用从启动页切换到主页面时会闪烁"通过动画协调实现防闪烁效果
"认证状态应该存在哪里?"采用应用级状态机,避免分散的布尔值
"如何处理应用进入后台的情况?"提供scenePhase生命周期处理模式
"何时应该将应用拆分为模块?"基于代码库规模和团队规模的决策树
"应用被杀死后如何恢复状态?"SceneStorage与状态验证模式

Quick Decision Tree

快速决策树

What app-level architecture question are you solving?
├─ How do I manage app states (loading, auth, main)?
│  └─ Part 1: App-Level State Machines
│     - Enum-based state with validated transitions
│     - AppStateController pattern
│     - Prevents "boolean soup" anti-pattern
├─ How do I structure @main and root view switching?
│  └─ Part 2: Root View Switching Patterns
│     - Delegate to AppStateController (no logic in @main)
│     - Flicker prevention with animation
│     - Coordinator integration
├─ How do I handle scene lifecycle?
│  └─ Part 3: Scene Lifecycle Integration
│     - scenePhase for session validation
│     - SceneStorage for restoration
│     - Multi-window coordination
├─ When should I modularize?
│  └─ Part 4: Feature Module Basics
│     - Decision tree by size/team
│     - Module boundaries and DI
│     - Navigation coordination
└─ What mistakes should I avoid?
   └─ Part 5: Anti-Patterns + Part 6: Pressure Scenarios
      - Boolean-based state
      - Logic in @main
      - Missing restoration validation

你要解决的是哪类应用级架构问题?
├─ 如何管理应用状态(加载中、未认证、已认证)?
│  └─ 第一部分:应用级状态机
│     - 基于枚举的状态与经过验证的过渡
│     - AppStateController模式
│     - 避免“布尔值混乱”反模式
├─ 如何构建@main与根视图切换逻辑?
│  └─ 第二部分:根视图切换模式
│     - 将逻辑委托给AppStateController(@main中无业务逻辑)
│     - 带动画的防闪烁处理
│     - 协调器集成
├─ 如何处理场景生命周期?
│  └─ 第三部分:场景生命周期集成
│     - 用scenePhase验证会话
│     - 用SceneStorage实现状态恢复
│     - 多窗口协调
├─ 何时应该进行模块化?
│  └─ 第四部分:功能模块基础
│     - 基于规模/团队的决策树
│     - 模块边界与依赖注入
│     - 导航协调
└─ 应该避免哪些错误?
   └─ 第五部分:反模式 + 第六部分:压力场景
      - 基于布尔值的状态管理
      - @main中包含业务逻辑
      - 缺失状态恢复验证

Part 1: App-Level State Machines

第一部分:应用级状态机

Core Principle

核心原则

"Apps have discrete states. Model them explicitly with enums, not scattered booleans."
Every non-trivial app has distinct states: loading, unauthenticated, onboarding, authenticated, error recovery. These states should be:
  1. Explicit — An enum, not multiple booleans
  2. Validated — Transitions are checked and logged
  3. Centralized — One source of truth
  4. Observable — Views react to state changes
"应用具有离散状态。使用枚举显式建模,而非分散的布尔值。"
所有非简单应用都有不同的状态:加载中、未认证、引导页、已认证、错误恢复。这些状态应满足:
  1. 显式化 — 使用枚举,而非多个布尔值
  2. 可验证 — 状态过渡需经过检查并记录
  3. 集中化 — 单一数据源
  4. 可观察 — 视图响应状态变化

The Boolean Soup Problem

布尔值混乱问题

swift
// ❌ Boolean soup — impossible to validate, prone to invalid states
class AppState {
    var isLoading = true
    var isLoggedIn = false
    var hasCompletedOnboarding = false
    var hasError = false
    var user: User?

    // What if isLoading && isLoggedIn && hasError are all true?
    // Invalid state, but nothing prevents it
}
Problems
  • No compile-time guarantee of valid states
  • Easy to forget to update one boolean
  • Testing requires checking all combinations
  • Race conditions create impossible states
swift
// ❌ 布尔值混乱——无法验证,易出现无效状态
class AppState {
    var isLoading = true
    var isLoggedIn = false
    var hasCompletedOnboarding = false
    var hasError = false
    var user: User?

    // 如果isLoading && isLoggedIn && hasError同时为true会怎样?
    // 这是无效状态,但没有任何机制阻止它
}
问题
  • 无法通过编译保证状态有效性
  • 容易忘记更新某个布尔值
  • 测试需要检查所有组合情况
  • 竞态条件会导致不可能的状态

The AppStateController Pattern

AppStateController模式

Step 1: Define Explicit States

步骤1:定义显式状态

swift
enum AppState: Equatable {
    case loading
    case unauthenticated
    case onboarding(OnboardingStep)
    case authenticated(User)
    case error(AppError)
}

enum OnboardingStep: Equatable {
    case welcome
    case permissions
    case profileSetup
    case complete
}

enum AppError: Equatable {
    case networkUnavailable
    case sessionExpired
    case maintenanceMode
}
swift
enum AppState: Equatable {
    case loading
    case unauthenticated
    case onboarding(OnboardingStep)
    case authenticated(User)
    case error(AppError)
}

enum OnboardingStep: Equatable {
    case welcome
    case permissions
    case profileSetup
    case complete
}

enum AppError: Equatable {
    case networkUnavailable
    case sessionExpired
    case maintenanceMode
}

Step 2: Create the Controller

步骤2:创建控制器

swift
@Observable
@MainActor
class AppStateController {
    private(set) var state: AppState = .loading

    // MARK: - State Transitions

    func transition(to newState: AppState) {
        guard isValidTransition(from: state, to: newState) else {
            assertionFailure("Invalid transition: \(state)\(newState)")
            logInvalidTransition(from: state, to: newState)
            return
        }

        let oldState = state
        state = newState
        logTransition(from: oldState, to: newState)
    }

    // MARK: - Validation

    private func isValidTransition(from: AppState, to: AppState) -> Bool {
        switch (from, to) {
        // From loading
        case (.loading, .unauthenticated): return true
        case (.loading, .authenticated): return true
        case (.loading, .error): return true

        // From unauthenticated
        case (.unauthenticated, .onboarding): return true
        case (.unauthenticated, .authenticated): return true
        case (.unauthenticated, .error): return true

        // From onboarding
        case (.onboarding, .onboarding): return true  // Step changes
        case (.onboarding, .authenticated): return true
        case (.onboarding, .unauthenticated): return true  // Cancelled

        // From authenticated
        case (.authenticated, .unauthenticated): return true  // Logout
        case (.authenticated, .error): return true

        // From error
        case (.error, .loading): return true  // Retry
        case (.error, .unauthenticated): return true

        default: return false
        }
    }

    // MARK: - Logging

    private func logTransition(from: AppState, to: AppState) {
        #if DEBUG
        print("AppState: \(from)\(to)")
        #endif
    }

    private func logInvalidTransition(from: AppState, to: AppState) {
        // Log to analytics for debugging
        Analytics.log("InvalidStateTransition", properties: [
            "from": String(describing: from),
            "to": String(describing: to)
        ])
    }
}
swift
@Observable
@MainActor
class AppStateController {
    private(set) var state: AppState = .loading

    // MARK: - 状态过渡

    func transition(to newState: AppState) {
        guard isValidTransition(from: state, to: newState) else {
            assertionFailure("无效过渡:\(state)\(newState)")
            logInvalidTransition(from: state, to: newState)
            return
        }

        let oldState = state
        state = newState
        logTransition(from: oldState, to: newState)
    }

    // MARK: - 验证

    private func isValidTransition(from: AppState, to: AppState) -> Bool {
        switch (from, to) {
        // 从加载中状态
        case (.loading, .unauthenticated): return true
        case (.loading, .authenticated): return true
        case (.loading, .error): return true

        // 从未认证状态
        case (.unauthenticated, .onboarding): return true
        case (.unauthenticated, .authenticated): return true
        case (.unauthenticated, .error): return true

        // 从引导页状态
        case (.onboarding, .onboarding): return true  // 步骤切换
        case (.onboarding, .authenticated): return true
        case (.onboarding, .unauthenticated): return true  // 取消引导

        // 从已认证状态
        case (.authenticated, .unauthenticated): return true  // 登出
        case (.authenticated, .error): return true

        // 从错误状态
        case (.error, .loading): return true  // 重试
        case (.error, .unauthenticated): return true

        default: return false
        }
    }

    // MARK: - 日志

    private func logTransition(from: AppState, to: AppState) {
        #if DEBUG
        print("AppState: \(from)\(to)")
        #endif
    }

    private func logInvalidTransition(from: AppState, to: AppState) {
        // 记录到分析工具用于调试
        Analytics.log("InvalidStateTransition", properties: [
            "from": String(describing: from),
            "to": String(describing: to)
        ])
    }
}

Step 3: Initialize from Storage

步骤3:从存储初始化

swift
extension AppStateController {
    func initialize() async {
        // Check for stored session
        if let session = await SessionStorage.loadSession() {
            // Validate session is still valid
            do {
                let user = try await AuthService.validateSession(session)
                transition(to: .authenticated(user))
            } catch {
                // Session expired or invalid
                await SessionStorage.clearSession()
                transition(to: .unauthenticated)
            }
        } else {
            transition(to: .unauthenticated)
        }
    }
}
swift
extension AppStateController {
    func initialize() async {
        // 检查存储的会话
        if let session = await SessionStorage.loadSession() {
            // 验证会话是否仍有效
            do {
                let user = try await AuthService.validateSession(session)
                transition(to: .authenticated(user))
            } catch {
                // 会话过期或无效
                await SessionStorage.clearSession()
                transition(to: .unauthenticated)
            }
        } else {
            transition(to: .unauthenticated)
        }
    }
}

State Machine Diagram

状态机图

┌─────────────────────────────────────────────────────────────┐
│                        .loading                              │
└────────────┬───────────────┬────────────────┬───────────────┘
             │               │                │
             ▼               ▼                ▼
    .unauthenticated   .authenticated     .error
             │               │                │
             ▼               │                │
       .onboarding ─────────►│◄───────────────┘
             │               │
             └───────────────┘
┌─────────────────────────────────────────────────────────────┐
│                        .loading                              │
└────────────┬───────────────┬────────────────┬───────────────┘
             │               │                │
             ▼               ▼                ▼
    .unauthenticated   .authenticated     .error
             │               │                │
             ▼               │                │
       .onboarding ─────────►│◄───────────────┘
             │               │
             └───────────────┘

Testing State Machines

测试状态机

swift
@Test func testValidTransitions() async {
    let controller = AppStateController()

    // Loading → Unauthenticated (valid)
    controller.transition(to: .unauthenticated)
    #expect(controller.state == .unauthenticated)

    // Unauthenticated → Authenticated (valid)
    let user = User(id: "1", name: "Test")
    controller.transition(to: .authenticated(user))
    #expect(controller.state == .authenticated(user))
}

@Test func testInvalidTransitionRejected() async {
    let controller = AppStateController()

    // Loading → Onboarding (invalid — must go through unauthenticated)
    controller.transition(to: .onboarding(.welcome))
    #expect(controller.state == .loading)  // Unchanged
}

@Test func testSessionExpiredTransition() async {
    let controller = AppStateController()
    let user = User(id: "1", name: "Test")
    controller.transition(to: .authenticated(user))

    // Authenticated → Error (session expired)
    controller.transition(to: .error(.sessionExpired))
    #expect(controller.state == .error(.sessionExpired))

    // Error → Unauthenticated (force re-login)
    controller.transition(to: .unauthenticated)
    #expect(controller.state == .unauthenticated)
}
swift
@Test func testValidTransitions() async {
    let controller = AppStateController()

    // 加载中 → 未认证(有效)
    controller.transition(to: .unauthenticated)
    #expect(controller.state == .unauthenticated)

    // 未认证 → 已认证(有效)
    let user = User(id: "1", name: "Test")
    controller.transition(to: .authenticated(user))
    #expect(controller.state == .authenticated(user))
}

@Test func testInvalidTransitionRejected() async {
    let controller = AppStateController()

    // 加载中 → 引导页(无效——必须先经过未认证状态)
    controller.transition(to: .onboarding(.welcome))
    #expect(controller.state == .loading)  // 状态未改变
}

@Test func testSessionExpiredTransition() async {
    let controller = AppStateController()
    let user = User(id: "1", name: "Test")
    controller.transition(to: .authenticated(user))

    // 已认证 → 错误(会话过期)
    controller.transition(to: .error(.sessionExpired))
    #expect(controller.state == .error(.sessionExpired))

    // 错误 → 未认证(强制重新登录)
    controller.transition(to: .unauthenticated)
    #expect(controller.state == .unauthenticated)
}

The State-as-Bridge Pattern (WWDC 2025/266)

状态桥接模式(WWDC 2025/266)

From WWDC 2025's "Explore concurrency in SwiftUI":
"Find the boundaries between UI code that requires time-sensitive changes, and long-running async logic."
The key insight: synchronous state changes drive UI (for animations), async code lives in the model (testable without SwiftUI), and state bridges the two.
swift
// ✅ State-as-Bridge: UI triggers state, model does async work
struct ColorExtractorView: View {
    @State private var model = ColorExtractor()

    var body: some View {
        Button("Extract Colors") {
            // ✅ Synchronous state change triggers animation
            withAnimation { model.isExtracting = true }

            // Async work happens in Task
            Task {
                await model.extractColors()

                // ✅ Synchronous state change ends animation
                withAnimation { model.isExtracting = false }
            }
        }
        .scaleEffect(model.isExtracting ? 1.5 : 1.0)
    }
}

@Observable
class ColorExtractor {
    var isExtracting = false
    var colors: [Color] = []

    func extractColors() async {
        // Heavy computation happens here, testable without SwiftUI
        let extracted = await heavyComputation()
        colors = extracted
    }
}
Why this matters for app composition
  • App-level state changes (loading → authenticated) should be synchronous
  • Heavy work (session validation, data loading) should be async in the model
  • This separation makes state machines testable without SwiftUI imports

来自WWDC 2025的《探索SwiftUI中的并发》:
"找到需要时间敏感变更的UI代码,与长时间运行的异步逻辑之间的边界。"
核心见解:同步状态变更驱动UI(用于动画),异步代码存放在模型中(无需SwiftUI即可测试),状态作为两者的桥梁
swift
// ✅ 状态桥接:UI触发状态变更,模型执行异步工作
struct ColorExtractorView: View {
    @State private var model = ColorExtractor()

    var body: some View {
        Button("提取颜色") {
            // ✅ 同步状态变更触发动画
            withAnimation { model.isExtracting = true }

            // 异步工作在Task中执行
            Task {
                await model.extractColors()

                // ✅ 同步状态变更结束动画
                withAnimation { model.isExtracting = false }
            }
        }
        .scaleEffect(model.isExtracting ? 1.5 : 1.0)
    }
}

@Observable
class ColorExtractor {
    var isExtracting = false
    var colors: [Color] = []

    func extractColors() async {
        // 繁重计算在此执行,无需SwiftUI即可测试
        let extracted = await heavyComputation()
        colors = extracted
    }
}
这对应用组合的重要性
  • 应用级状态变更(加载中 → 已认证)应该是同步
  • 繁重工作(会话验证、数据加载)应该在模型中异步执行
  • 这种分离让状态机无需导入SwiftUI即可测试

Part 2: Root View Switching Patterns

第二部分:根视图切换模式

Core Principle

核心原则

"The @main entry point should be a thin shell. All logic belongs in AppStateController."
"@main入口点应该是一个轻量壳层。所有逻辑都应放在AppStateController中。"

The Clean @main Pattern

简洁@main模式

swift
@main
struct MyApp: App {
    @State private var appState = AppStateController()

    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(appState)
                .task {
                    await appState.initialize()
                }
        }
    }
}
What @main does
  • Creates AppStateController
  • Injects it via environment
  • Triggers initialization
What @main does NOT do
  • Business logic
  • Auth checks
  • Conditional rendering
  • Navigation decisions
swift
@main
struct MyApp: App {
    @State private var appState = AppStateController()

    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(appState)
                .task {
                    await appState.initialize()
                }
        }
    }
}
@main的职责
  • 创建AppStateController
  • 通过环境注入控制器
  • 触发初始化
@main不负责的内容
  • 业务逻辑
  • 认证检查
  • 条件渲染
  • 导航决策

RootView: The State Switch

RootView:状态切换器

swift
struct RootView: View {
    @Environment(AppStateController.self) private var appState

    var body: some View {
        Group {
            switch appState.state {
            case .loading:
                LaunchView()
            case .unauthenticated:
                AuthenticationFlow()
            case .onboarding(let step):
                OnboardingFlow(step: step)
            case .authenticated(let user):
                MainTabView(user: user)
            case .error(let error):
                ErrorRecoveryView(error: error)
            }
        }
    }
}
swift
struct RootView: View {
    @Environment(AppStateController.self) private var appState

    var body: some View {
        Group {
            switch appState.state {
            case .loading:
                LaunchView()
            case .unauthenticated:
                AuthenticationFlow()
            case .onboarding(let step):
                OnboardingFlow(step: step)
            case .authenticated(let user):
                MainTabView(user: user)
            case .error(let error):
                ErrorRecoveryView(error: error)
            }
        }
    }
}

Preventing Flicker During Transitions

避免过渡时的闪烁

Problem: Flash of Wrong Content

问题:错误内容闪现

When app state changes, you might see a flash of the old screen before the new one appears. This happens when:
  • State changes before view is ready
  • No transition animation
  • Loading state too short to perceive
当应用状态变更时,你可能会在新页面出现前看到旧页面的闪屏。这通常是因为:
  • 状态在视图准备好之前就已变更
  • 没有过渡动画
  • 加载状态持续时间太短,用户无法感知

Solution: Animated Transitions

解决方案:动画过渡

swift
struct RootView: View {
    @Environment(AppStateController.self) private var appState

    var body: some View {
        ZStack {
            switch appState.state {
            case .loading:
                LaunchView()
                    .transition(.opacity)
            case .unauthenticated:
                AuthenticationFlow()
                    .transition(.opacity)
            case .onboarding(let step):
                OnboardingFlow(step: step)
                    .transition(.opacity)
            case .authenticated(let user):
                MainTabView(user: user)
                    .transition(.opacity)
            case .error(let error):
                ErrorRecoveryView(error: error)
                    .transition(.opacity)
            }
        }
        .animation(.easeInOut(duration: 0.3), value: appState.state)
    }
}
swift
struct RootView: View {
    @Environment(AppStateController.self) private var appState

    var body: some View {
        ZStack {
            switch appState.state {
            case .loading:
                LaunchView()
                    .transition(.opacity)
            case .unauthenticated:
                AuthenticationFlow()
                    .transition(.opacity)
            case .onboarding(let step):
                OnboardingFlow(step: step)
                    .transition(.opacity)
            case .authenticated(let user):
                MainTabView(user: user)
                    .transition(.opacity)
            case .error(let error):
                ErrorRecoveryView(error: error)
                    .transition(.opacity)
            }
        }
        .animation(.easeInOut(duration: 0.3), value: appState.state)
    }
}

Minimum Loading Duration

最小加载时长

For a polished experience, ensure the loading screen is visible long enough:
swift
extension AppStateController {
    func initialize() async {
        let startTime = Date()

        // Do actual initialization
        await performInitialization()

        // Ensure minimum display time for loading screen
        let elapsed = Date().timeIntervalSince(startTime)
        let minimumDuration: TimeInterval = 0.5
        if elapsed < minimumDuration {
            try? await Task.sleep(for: .seconds(minimumDuration - elapsed))
        }
    }
}
为了获得流畅的体验,确保加载屏幕显示足够长的时间:
swift
extension AppStateController {
    func initialize() async {
        let startTime = Date()

        // 执行实际初始化操作
        await performInitialization()

        // 确保加载屏幕的最小显示时长
        let elapsed = Date().timeIntervalSince(startTime)
        let minimumDuration: TimeInterval = 0.5
        if elapsed < minimumDuration {
            try? await Task.sleep(for: .seconds(minimumDuration - elapsed))
        }
    }
}

Coordinator Integration

协调器集成

If using coordinators, integrate them at the root level:
swift
struct RootView: View {
    @Environment(AppStateController.self) private var appState
    @State private var authCoordinator = AuthCoordinator()
    @State private var mainCoordinator = MainCoordinator()

    var body: some View {
        Group {
            switch appState.state {
            case .loading:
                LaunchView()
            case .unauthenticated, .onboarding:
                AuthenticationFlow()
                    .environment(authCoordinator)
            case .authenticated(let user):
                MainTabView(user: user)
                    .environment(mainCoordinator)
            case .error(let error):
                ErrorRecoveryView(error: error)
            }
        }
        .animation(.easeInOut(duration: 0.3), value: appState.state)
    }
}

如果使用协调器,在根级别集成:
swift
struct RootView: View {
    @Environment(AppStateController.self) private var appState
    @State private var authCoordinator = AuthCoordinator()
    @State private var mainCoordinator = MainCoordinator()

    var body: some View {
        Group {
            switch appState.state {
            case .loading:
                LaunchView()
            case .unauthenticated, .onboarding:
                AuthenticationFlow()
                    .environment(authCoordinator)
            case .authenticated(let user):
                MainTabView(user: user)
                    .environment(mainCoordinator)
            case .error(let error):
                ErrorRecoveryView(error: error)
            }
        }
        .animation(.easeInOut(duration: 0.3), value: appState.state)
    }
}

Part 3: Scene Lifecycle Integration

第三部分:场景生命周期集成

Core Principle

核心原则

"Scene lifecycle events are app-wide concerns handled centrally, not scattered across features."
"场景生命周期事件是应用级关注点,应集中处理,而非分散在各个功能中。"

Understanding ScenePhase (Apple Documentation)

理解ScenePhase(苹果官方文档)

ScenePhase indicates a scene's operational state. How you interpret the value depends on where it's read.
Read from a View → Returns the phase of the enclosing scene Read from App → Returns an aggregate value reflecting all scenes
PhaseDescription
.active
Scene is in the foreground and interactive
.inactive
Scene is in the foreground but should pause work
.background
Scene isn't visible; app may terminate soon
Critical insight from Apple docs When reading at the App level,
.active
means any scene is active, and
.background
means all scenes are in background.
ScenePhase表示场景的运行状态。如何解读该值取决于读取位置
在View中读取 → 返回当前场景的状态 在App中读取 → 返回反映所有场景的聚合
状态描述
.active
场景处于前台且可交互
.inactive
场景处于前台但应暂停工作
.background
场景不可见;应用可能即将终止
苹果文档的关键见解 在App级别读取时,
.active
表示任意场景处于活跃状态,
.background
表示所有场景都处于后台。

scenePhase Handling

scenePhase处理

swift
@main
struct MyApp: App {
    @State private var appState = AppStateController()
    @Environment(\.scenePhase) private var scenePhase

    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(appState)
                .task {
                    await appState.initialize()
                }
        }
        .onChange(of: scenePhase) { oldPhase, newPhase in
            handleScenePhaseChange(from: oldPhase, to: newPhase)
        }
    }

    private func handleScenePhaseChange(from: ScenePhase, to: ScenePhase) {
        switch to {
        case .active:
            // App became active — validate session, refresh data
            Task {
                await appState.validateSession()
                await appState.refreshIfNeeded()
            }

        case .inactive:
            // App about to go inactive — save state
            appState.prepareForBackground()

        case .background:
            // App in background — release resources
            appState.releaseResources()

        @unknown default:
            break
        }
    }
}
swift
@main
struct MyApp: App {
    @State private var appState = AppStateController()
    @Environment(\.scenePhase) private var scenePhase

    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(appState)
                .task {
                    await appState.initialize()
                }
        }
        .onChange(of: scenePhase) { oldPhase, newPhase in
            handleScenePhaseChange(from: oldPhase, to: newPhase)
        }
    }

    private func handleScenePhaseChange(from: ScenePhase, to: ScenePhase) {
        switch to {
        case .active:
            // 应用变为活跃状态——验证会话,刷新数据
            Task {
                await appState.validateSession()
                await appState.refreshIfNeeded()
            }

        case .inactive:
            // 应用即将变为非活跃状态——保存状态
            appState.prepareForBackground()

        case .background:
            // 应用处于后台——释放资源
            appState.releaseResources()

        @unknown default:
            break
        }
    }
}

Session Validation on Active

活跃时验证会话

swift
extension AppStateController {
    func validateSession() async {
        guard case .authenticated(let user) = state else { return }

        do {
            // Check if token is still valid
            let isValid = try await AuthService.validateToken(user.token)
            if !isValid {
                transition(to: .error(.sessionExpired))
            }
        } catch {
            // Network error — keep authenticated but show warning
            // Don't immediately log out on transient network issues
        }
    }

    func prepareForBackground() {
        // Save any pending data
        // Cancel non-essential network requests
        // Prepare for potential termination
    }

    func releaseResources() {
        // Release cached images
        // Stop location updates if not essential
        // Reduce memory footprint
    }
}
swift
extension AppStateController {
    func validateSession() async {
        guard case .authenticated(let user) = state else { return }

        do {
            // 检查令牌是否仍有效
            let isValid = try await AuthService.validateToken(user.token)
            if !isValid {
                transition(to: .error(.sessionExpired))
            }
        } catch {
            // 网络错误——保持已认证状态但显示警告
            // 不要因临时网络问题立即登出
        }
    }

    func prepareForBackground() {
        // 保存所有未提交的数据
        // 取消非必要的网络请求
        // 为可能的终止做准备
    }

    func releaseResources() {
        // 释放缓存图片
        // 如果非必需则停止位置更新
        // 减少内存占用
    }
}

SceneStorage for State Restoration

SceneStorage用于状态恢复

From Apple documentation: SceneStorage provides automatic state restoration. The system manages saving and restoring on your behalf.
Key constraints
  • Keep data lightweight (not full models)
  • Each Scene has its own storage (not shared)
  • Data destroyed when scene is explicitly destroyed
swift
struct MainTabView: View {
    @SceneStorage("selectedTab") private var selectedTab = 0
    @SceneStorage("lastViewedItemID") private var lastViewedItemID: String?

    var body: some View {
        TabView(selection: $selectedTab) {
            HomeTab()
                .tag(0)
            SearchTab()
                .tag(1)
            ProfileTab()
                .tag(2)
        }
        .onAppear {
            if let itemID = lastViewedItemID {
                // Restore to last viewed item
                navigateToItem(itemID)
            }
        }
    }
}
来自苹果官方文档:SceneStorage提供自动状态恢复。系统会代表你管理状态的保存与恢复。
关键约束
  • 保持数据轻量化(不要存储完整模型)
  • 每个Scene都有自己的存储(不共享)
  • 当场景被显式销毁时,数据会被清除
swift
struct MainTabView: View {
    @SceneStorage("selectedTab") private var selectedTab = 0
    @SceneStorage("lastViewedItemID") private var lastViewedItemID: String?

    var body: some View {
        TabView(selection: $selectedTab) {
            HomeTab()
                .tag(0)
            SearchTab()
                .tag(1)
            ProfileTab()
                .tag(2)
        }
        .onAppear {
            if let itemID = lastViewedItemID {
                // 恢复到最后查看的项目
                navigateToItem(itemID)
            }
        }
    }
}

Navigation State Restoration (WWDC 2022/10054)

导航状态恢复(WWDC 2022/10054)

For complex navigation, use a Codable NavigationModel:
swift
// Encapsulate navigation state with Codable conformance
class NavigationModel: ObservableObject, Codable {
    @Published var selectedCategory: Category?
    @Published var recipePath: [Recipe] = []

    enum CodingKeys: String, CodingKey {
        case selectedCategory
        case recipePathIds
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
        // Store only IDs, not full models
        try container.encode(recipePath.map(\.id), forKey: .recipePathIds)
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.selectedCategory = try container.decodeIfPresent(
            Category.self, forKey: .selectedCategory)

        let recipePathIds = try container.decode([Recipe.ID].self, forKey: .recipePathIds)
        // compactMap discards deleted items gracefully
        self.recipePath = recipePathIds.compactMap { DataModel.shared[$0] }
    }

    var jsonData: Data? {
        get { try? JSONEncoder().encode(self) }
        set {
            guard let data = newValue,
                  let model = try? JSONDecoder().decode(NavigationModel.self, from: data)
            else { return }
            self.selectedCategory = model.selectedCategory
            self.recipePath = model.recipePath
        }
    }
}

// Use with SceneStorage
struct ContentView: View {
    @StateObject private var navModel = NavigationModel()
    @SceneStorage("navigation") private var data: Data?

    var body: some View {
        NavigationSplitView { /* ... */ }
        .task {
            if let data = data {
                navModel.jsonData = data
            }
            for await _ in navModel.objectWillChangeSequence {
                data = navModel.jsonData
            }
        }
    }
}
Key patterns from WWDC
  • Store IDs only, not full model objects
  • Use
    compactMap
    to handle deleted items gracefully
  • Save on every
    objectWillChange
    for real-time persistence
对于复杂导航,使用可编码的NavigationModel:
swift
// 用Codable封装导航状态
class NavigationModel: ObservableObject, Codable {
    @Published var selectedCategory: Category?
    @Published var recipePath: [Recipe] = []

    enum CodingKeys: String, CodingKey {
        case selectedCategory
        case recipePathIds
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
        // 仅存储ID,而非完整模型
        try container.encode(recipePath.map(\.id), forKey: .recipePathIds)
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.selectedCategory = try container.decodeIfPresent(
            Category.self, forKey: .selectedCategory)

        let recipePathIds = try container.decode([Recipe.ID].self, forKey: .recipePathIds)
        // compactMap优雅处理已删除的项目
        self.recipePath = recipePathIds.compactMap { DataModel.shared[$0] }
    }

    var jsonData: Data? {
        get { try? JSONEncoder().encode(self) }
        set {
            guard let data = newValue,
                  let model = try? JSONDecoder().decode(NavigationModel.self, from: data)
            else { return }
            self.selectedCategory = model.selectedCategory
            self.recipePath = model.recipePath
        }
    }
}

// 与SceneStorage配合使用
struct ContentView: View {
    @StateObject private var navModel = NavigationModel()
    @SceneStorage("navigation") private var data: Data?

    var body: some View {
        NavigationSplitView { /* ... */ }
        .task {
            if let data = data {
                navModel.jsonData = data
            }
            for await _ in navModel.objectWillChangeSequence {
                data = navModel.jsonData
            }
        }
    }
}
来自WWDC的关键模式
  • 仅存储ID,而非完整模型对象
  • 使用
    compactMap
    优雅处理已删除的项目
  • 在每次
    objectWillChange
    时保存,实现实时持久化

Validating Restored State

验证恢复的状态

Never trust restored state blindly:
swift
struct DetailView: View {
    @SceneStorage("detailItemID") private var restoredItemID: String?
    @State private var item: Item?

    var body: some View {
        Group {
            if let item {
                ItemContent(item: item)
            } else {
                ProgressView()
            }
        }
        .task {
            if let itemID = restoredItemID {
                // Validate item still exists
                item = await ItemService.fetch(itemID)
                if item == nil {
                    // Item was deleted — clear restoration
                    restoredItemID = nil
                }
            }
        }
    }
}
永远不要盲目信任恢复的状态:
swift
struct DetailView: View {
    @SceneStorage("detailItemID") private var restoredItemID: String?
    @State private var item: Item?

    var body: some View {
        Group {
            if let item {
                ItemContent(item: item)
            } else {
                ProgressView()
            }
        }
        .task {
            if let itemID = restoredItemID {
                // 验证项目是否仍存在
                item = await ItemService.fetch(itemID)
                if item == nil {
                    // 项目已被删除——清除恢复状态
                    restoredItemID = nil
                }
            }
        }
    }
}

Multi-Window Coordination (iPad, axiom-visionOS)

多窗口协调(iPad、visionOS)

From Apple documentation: Every window in a WindowGroup maintains independent state. The system allocates new storage for @State and @StateObject for each window.
swift
@main
struct MyApp: App {
    @State private var appState = AppStateController()

    var body: some Scene {
        // Primary window
        WindowGroup {
            MainView()
                .environment(appState)
        }

        // Data-presenting window (iPad)
        // Prefer lightweight data (IDs, not full models)
        WindowGroup("Detail", id: "detail", for: Item.ID.self) { $itemID in
            if let itemID {
                DetailView(itemID: itemID)
                    .environment(appState)
            }
        }

        #if os(visionOS)
        // Immersive space
        ImmersiveSpace(id: "immersive") {
            ImmersiveView()
                .environment(appState)
        }
        #endif
    }
}
Key behaviors from Apple docs
  • If a window with the same value already exists, the system brings it to front instead of opening a new one
  • SwiftUI persists the binding value for state restoration
  • Use unique identifier strings for each window group
来自苹果官方文档:WindowGroup中的每个窗口都保持独立状态。系统会为每个窗口的@State和@StateObject分配新的存储。
swift
@main
struct MyApp: App {
    @State private var appState = AppStateController()

    var body: some Scene {
        // 主窗口
        WindowGroup {
            MainView()
                .environment(appState)
        }

        // 数据展示窗口(iPad)
        // 优先使用轻量数据(ID,而非完整模型)
        WindowGroup("Detail", id: "detail", for: Item.ID.self) { $itemID in
            if let itemID {
                DetailView(itemID: itemID)
                    .environment(appState)
            }
        }

        #if os(visionOS)
        // 沉浸式空间
        ImmersiveSpace(id: "immersive") {
            ImmersiveView()
                .environment(appState)
        }
        #endif
    }
}
来自苹果文档的关键行为
  • 如果具有相同值的窗口已存在,系统会将其前置而非打开新窗口
  • SwiftUI会持久化绑定值以实现状态恢复
  • 为每个窗口组使用唯一的标识符字符串

Opening Additional Windows

打开额外窗口

swift
struct ItemRow: View {
    let item: Item
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button(item.title) {
            // Open in new window on iPad
            // Use ID to match window group, value to pass data
            openWindow(id: "detail", value: item.id)
        }
    }
}
swift
struct ItemRow: View {
    let item: Item
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button(item.title) {
            // 在iPad上打开新窗口
            // 使用ID匹配窗口组,使用值传递数据
            openWindow(id: "detail", value: item.id)
        }
    }
}

Dismissing Windows Programmatically

以编程方式关闭窗口

swift
struct DetailView: View {
    var itemID: Item.ID?
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        VStack {
            // ...
            Button("Done") {
                dismiss()  // Closes this window
            }
        }
    }
}

swift
struct DetailView: View {
    var itemID: Item.ID?
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        VStack {
            // ...
            Button("完成") {
                dismiss()  // 关闭当前窗口
            }
        }
    }
}

Part 4: Feature Module Basics

第四部分:功能模块基础

Core Principle

核心原则

"Split into modules when features have clear boundaries. Not before."
Premature modularization creates overhead. Late modularization creates pain. Use this decision tree.
"当功能具有清晰边界时再拆分为模块。不要过早模块化。"
过早模块化会增加开销,过晚模块化会带来痛苦。使用以下决策树。

When to Modularize Decision Tree

何时模块化决策树

Should I extract this feature into a module?
├─ Is the codebase under 5,000 lines with 1-2 developers?
│  └─ NO modularization needed yet
│     Single target is fine, revisit at 10,000 lines
├─ Is the codebase 5,000-20,000 lines with 3+ developers?
│  └─ CONSIDER modularization
│     Look for natural boundaries
├─ Is the codebase over 20,000 lines?
│  └─ MODULARIZE for build times
│     Parallel compilation essential
├─ Could this feature be used in multiple apps?
│  └─ EXTRACT to reusable module
│     Shared authentication, analytics, axiom-networking
├─ Do multiple developers work on this feature daily?
│  └─ EXTRACT for merge conflict reduction
│     Isolated codebases = parallel work
└─ Does the feature have clear input/output boundaries?
   ├─ YES → Good candidate for module
   └─ NO → Refactor boundaries first, then extract
我应该将该功能提取为模块吗?
├─ 代码库少于5000行且团队规模为1-2人?
│  └─ 暂时不需要模块化
│     单一目标即可,在代码库达到10000行时重新评估
├─ 代码库为5000-20000行且团队规模为3人以上?
│  └─ 考虑模块化
│     寻找自然边界
├─ 代码库超过20000行?
│  └─ 必须模块化以优化构建时间
│     并行编译至关重要
├─ 该功能是否可在多个应用中复用?
│  └─ 提取为可复用模块
│     共享认证、分析、axiom-networking等
├─ 多个开发者每天都在处理该功能?
│  └─ 提取模块以减少合并冲突
│     隔离的代码库支持并行开发
└─ 该功能是否具有清晰的输入/输出边界?
   ├─ 是 → 适合作为模块
   └─ 否 → 先重构边界,再提取

Module Boundary Pattern

模块边界模式

Define a Public API

定义公共API

swift
// FeatureModule/Sources/FeatureModule/FeatureAPI.swift

/// Public interface for the feature module
public protocol FeatureAPI {
    /// Show the feature's main view
    @MainActor
    func makeMainView() -> AnyView

    /// Handle deep link into feature
    @MainActor
    func handleDeepLink(_ url: URL) -> Bool
}

/// Factory to create feature with dependencies
public struct FeatureFactory {
    public static func create(
        analytics: AnalyticsProtocol,
        networking: NetworkingProtocol
    ) -> FeatureAPI {
        FeatureImplementation(
            analytics: analytics,
            networking: axiom-networking
        )
    }
}
swift
// FeatureModule/Sources/FeatureModule/FeatureAPI.swift

/// 功能模块的公共接口
public protocol FeatureAPI {
    /// 展示功能的主视图
    @MainActor
    func makeMainView() -> AnyView

    /// 处理进入功能的深度链接
    @MainActor
    func handleDeepLink(_ url: URL) -> Bool
}

/// 用于创建带依赖的功能的工厂
public struct FeatureFactory {
    public static func create(
        analytics: AnalyticsProtocol,
        networking: NetworkingProtocol
    ) -> FeatureAPI {
        FeatureImplementation(
            analytics: analytics,
            networking: axiom-networking
        )
    }
}

Internal Implementation

内部实现

swift
// FeatureModule/Sources/FeatureModule/Internal/FeatureImplementation.swift

internal class FeatureImplementation: FeatureAPI {
    private let analytics: AnalyticsProtocol
    private let networking: NetworkingProtocol

    internal init(
        analytics: AnalyticsProtocol,
        networking: NetworkingProtocol
    ) {
        self.analytics = analytics
        self.networking = networking
    }

    @MainActor
    public func makeMainView() -> AnyView {
        AnyView(FeatureMainView(viewModel: makeViewModel()))
    }

    public func handleDeepLink(_ url: URL) -> Bool {
        // Handle feature-specific deep links
        return false
    }

    private func makeViewModel() -> FeatureViewModel {
        FeatureViewModel(analytics: analytics, axiom-networking: axiom-networking)
    }
}
swift
// FeatureModule/Sources/FeatureModule/Internal/FeatureImplementation.swift

internal class FeatureImplementation: FeatureAPI {
    private let analytics: AnalyticsProtocol
    private let networking: NetworkingProtocol

    internal init(
        analytics: AnalyticsProtocol,
        networking: NetworkingProtocol
    ) {
        self.analytics = analytics
        self.networking = networking
    }

    @MainActor
    public func makeMainView() -> AnyView {
        AnyView(FeatureMainView(viewModel: makeViewModel()))
    }

    public func handleDeepLink(_ url: URL) -> Bool {
        // 处理功能特定的深度链接
        return false
    }

    private func makeViewModel() -> FeatureViewModel {
        FeatureViewModel(analytics: analytics, axiom-networking: axiom-networking)
    }
}

Use in Main App

在主应用中使用

swift
// MainApp/Sources/App/AppDependencies.swift

@Observable
class AppDependencies {
    let analytics: AnalyticsProtocol
    let networking: NetworkingProtocol

    // Lazy-created feature modules
    lazy var profileFeature: FeatureAPI = {
        ProfileFeatureFactory.create(
            analytics: analytics,
            networking: axiom-networking
        )
    }()

    lazy var settingsFeature: FeatureAPI = {
        SettingsFeatureFactory.create(
            analytics: analytics,
            networking: axiom-networking
        )
    }()
}

// MainApp/Sources/App/MainTabView.swift

struct MainTabView: View {
    @Environment(AppDependencies.self) private var dependencies

    var body: some View {
        TabView {
            dependencies.profileFeature.makeMainView()
                .tabItem { Label("Profile", systemImage: "person") }

            dependencies.settingsFeature.makeMainView()
                .tabItem { Label("Settings", systemImage: "gear") }
        }
    }
}
swift
// MainApp/Sources/App/AppDependencies.swift

@Observable
class AppDependencies {
    let analytics: AnalyticsProtocol
    let networking: NetworkingProtocol

    // 懒加载的功能模块
    lazy var profileFeature: FeatureAPI = {
        ProfileFeatureFactory.create(
            analytics: analytics,
            networking: axiom-networking
        )
    }()

    lazy var settingsFeature: FeatureAPI = {
        SettingsFeatureFactory.create(
            analytics: analytics,
            networking: axiom-networking
        )
    }()
}

// MainApp/Sources/App/MainTabView.swift

struct MainTabView: View {
    @Environment(AppDependencies.self) private var dependencies

    var body: some View {
        TabView {
            dependencies.profileFeature.makeMainView()
                .tabItem { Label("Profile", systemImage: "person") }

            dependencies.settingsFeature.makeMainView()
                .tabItem { Label("Settings", systemImage: "gear") }
        }
    }
}

Navigation Coordination Between Modules

模块间的导航协调

Features should not know about each other directly:
swift
// ❌ Feature knows about other features
struct ProfileView: View {
    func showSettings() {
        // ProfileView imports SettingsFeature — circular dependency risk
        NavigationLink(value: SettingsDestination())
    }
}

// ✅ Feature delegates navigation to coordinator
struct ProfileView: View {
    let onShowSettings: () -> Void

    func showSettings() {
        onShowSettings()  // ProfileView doesn't know what happens
    }
}

// Coordinator wires features together
class MainCoordinator {
    func showSettings(from profile: ProfileFeatureAPI) {
        // Coordinator knows about both features
        navigationPath.append(SettingsRoute())
    }
}
功能之间不应直接相互依赖:
swift
// ❌ 功能依赖其他功能
struct ProfileView: View {
    func showSettings() {
        // ProfileView导入SettingsFeature——存在循环依赖风险
        NavigationLink(value: SettingsDestination())
    }
}

// ✅ 功能将导航委托给协调器
struct ProfileView: View {
    let onShowSettings: () -> Void

    func showSettings() {
        onShowSettings()  // ProfileView不知道具体会发生什么
    }
}

// 协调器连接各个功能
class MainCoordinator {
    func showSettings(from profile: ProfileFeatureAPI) {
        // 协调器了解所有功能
        navigationPath.append(SettingsRoute())
    }
}

Module Folder Structure

模块文件夹结构

MyApp/
├── App/                          # Main app target
│   ├── MyApp.swift               # @main entry point
│   ├── AppDependencies.swift     # Dependency container
│   ├── AppStateController.swift  # App state machine
│   └── Coordinators/             # Navigation coordinators
├── Packages/
│   ├── Core/                     # Shared utilities
│   │   ├── Networking/
│   │   ├── Analytics/
│   │   └── Design/               # Design system
│   │
│   ├── Features/                 # Feature modules
│   │   ├── Profile/
│   │   ├── Settings/
│   │   └── Onboarding/
│   │
│   └── Domain/                   # Business logic
│       ├── Models/
│       └── Services/

MyApp/
├── App/                          # 主应用目标
│   ├── MyApp.swift               # @main入口点
│   ├── AppDependencies.swift     # 依赖容器
│   ├── AppStateController.swift  # 应用状态机
│   └── Coordinators/             # 导航协调器
├── Packages/
│   ├── Core/                     # 共享工具
│   │   ├── Networking/
│   │   ├── Analytics/
│   │   └── Design/               # 设计系统
│   │
│   ├── Features/                 # 功能模块
│   │   ├── Profile/
│   │   ├── Settings/
│   │   └── Onboarding/
│   │
│   └── Domain/                   # 业务逻辑
│       ├── Models/
│       └── Services/

Part 5: Anti-Patterns

第五部分:反模式

Anti-Pattern 1: Boolean-Based State

反模式1:基于布尔值的状态管理

swift
// ❌ Boolean soup — impossible to validate
class AppState {
    var isLoading = true
    var isLoggedIn = false
    var hasCompletedOnboarding = false
    var hasError = false
}

// What if isLoading && isLoggedIn && hasError are all true?
Fix Use enum-based state (Part 1)
swift
// ✅ Explicit states — compiler prevents invalid combinations
enum AppState {
    case loading
    case unauthenticated
    case onboarding(OnboardingStep)
    case authenticated(User)
    case error(AppError)
}
swift
// ❌ 布尔值混乱——无法验证
class AppState {
    var isLoading = true
    var isLoggedIn = false
    var hasCompletedOnboarding = false
    var hasError = false
}

// 如果isLoading && isLoggedIn && hasError同时为true会怎样?
修复方案 使用基于枚举的状态(第一部分)
swift
// ✅ 显式状态——编译器阻止无效组合
enum AppState {
    case loading
    case unauthenticated
    case onboarding(OnboardingStep)
    case authenticated(User)
    case error(AppError)
}

Anti-Pattern 2: Logic in @main

反模式2:@main中包含逻辑

swift
// ❌ Business logic in App entry point
@main
struct MyApp: App {
    @State private var user: User?
    @State private var isLoading = true

    var body: some Scene {
        WindowGroup {
            if isLoading {
                LoadingView()
            } else if let user {
                MainView(user: user)
            } else {
                LoginView(onLogin: { self.user = $0 })
            }
        }
        .task {
            user = await AuthService.getCurrentUser()
            isLoading = false
        }
    }
}
Problems
  • @main becomes bloated with logic
  • Hard to test without launching app
  • State scattered across multiple @State
Fix Delegate to AppStateController (Part 2)
swift
// ✅ @main is a thin shell
@main
struct MyApp: App {
    @State private var appState = AppStateController()

    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(appState)
                .task { await appState.initialize() }
        }
    }
}
swift
// ❌ 业务逻辑存在于App入口点
@main
struct MyApp: App {
    @State private var user: User?
    @State private var isLoading = true

    var body: some Scene {
        WindowGroup {
            if isLoading {
                LoadingView()
            } else if let user {
                MainView(user: user)
            } else {
                LoginView(onLogin: { self.user = $0 })
            }
        }
        .task {
            user = await AuthService.getCurrentUser()
            isLoading = false
        }
    }
}
问题
  • @main因逻辑变得臃肿
  • 不启动应用则难以测试
  • 状态分散在多个@State中
修复方案 将逻辑委托给AppStateController(第二部分)
swift
// ✅ @main是轻量壳层
@main
struct MyApp: App {
    @State private var appState = AppStateController()

    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(appState)
                .task { await appState.initialize() }
        }
    }
}

Anti-Pattern 3: Missing State Validation on Restore

反模式3:恢复状态时缺失验证

swift
// ❌ Trusts restored state blindly
.onAppear {
    if let savedState = SceneStorage.appState {
        appState.state = savedState  // Token might be expired!
    }
}
Problems
  • Session could have expired
  • User could have been logged out on another device
  • Data could have been deleted
Fix Validate before applying (Part 3)
swift
// ✅ Validates restored state
.task {
    if let savedSession = await SessionStorage.loadSession() {
        do {
            let user = try await AuthService.validateSession(savedSession)
            appState.transition(to: .authenticated(user))
        } catch {
            // Session invalid — force re-login
            await SessionStorage.clearSession()
            appState.transition(to: .unauthenticated)
        }
    }
}
swift
// ❌ 盲目信任恢复的状态
.onAppear {
    if let savedState = SceneStorage.appState {
        appState.state = savedState  // 令牌可能已过期!
    }
}
问题
  • 会话可能已过期
  • 用户可能在其他设备上登出
  • 数据可能已被删除
修复方案 应用前先验证(第三部分)
swift
// ✅ 验证恢复的状态
.task {
    if let savedSession = await SessionStorage.loadSession() {
        do {
            let user = try await AuthService.validateSession(savedSession)
            appState.transition(to: .authenticated(user))
        } catch {
            // 会话无效——强制重新登录
            await SessionStorage.clearSession()
            appState.transition(to: .unauthenticated)
        }
    }
}

Anti-Pattern 4: Navigation Logic Scattered Across Features

反模式4:导航逻辑分散在各个功能中

swift
// ❌ Every feature knows about every other feature
struct ProfileView: View {
    @Environment(\.navigationPath) private var path

    func showSettings() {
        path.append(SettingsDestination())  // ProfileView imports Settings
    }

    func showOrderHistory() {
        path.append(OrderHistoryDestination())  // ProfileView imports Orders
    }
}
Problems
  • Circular dependencies
  • Hard to test navigation
  • Changes ripple across modules
Fix Delegate to coordinator (Part 4)
swift
// ✅ Feature delegates navigation decisions
struct ProfileView: View {
    let onShowSettings: () -> Void
    let onShowOrderHistory: () -> Void

    // ProfileView doesn't know what these do
}
swift
// ❌ 每个功能都了解其他所有功能
struct ProfileView: View {
    @Environment(\.navigationPath) private var path

    func showSettings() {
        path.append(SettingsDestination())  // ProfileView导入Settings
    }

    func showOrderHistory() {
        path.append(OrderHistoryDestination())  // ProfileView导入Orders
    }
}
问题
  • 循环依赖
  • 导航难以测试
  • 变更会影响多个模块
修复方案 将导航委托给协调器(第四部分)
swift
// ✅ 功能将导航决策委托出去
struct ProfileView: View {
    let onShowSettings: () -> Void
    let onShowOrderHistory: () -> Void

    // ProfileView不知道这些方法的具体实现
}

Anti-Pattern 5: God Coordinator

反模式5:上帝协调器

swift
// ❌ Single coordinator knows all features
class AppCoordinator {
    func showProfile() { }
    func showSettings() { }
    func showOnboarding() { }
    func showPayment() { }
    func showChat() { }
    func showOrderHistory() { }
    func showNotifications() { }
    // ... 50 more methods
}
Problems
  • Massive file that everyone touches
  • Merge conflicts
  • Single point of failure
Fix Scoped coordinators
swift
// ✅ Scoped coordinators for each domain
class AuthCoordinator { }      // Login, signup, forgot password
class MainCoordinator { }      // Tab navigation, main flows
class SettingsCoordinator { }  // Settings navigation tree
class OrderCoordinator { }     // Order flow, history, details

swift
// ❌ 单个协调器了解所有功能
class AppCoordinator {
    func showProfile() { }
    func showSettings() { }
    func showOnboarding() { }
    func showPayment() { }
    func showChat() { }
    func showOrderHistory() { }
    func showNotifications() { }
    // ... 还有50多个方法
}
问题
  • 所有人都要修改的巨型文件
  • 合并冲突
  • 单点故障
修复方案 范围化协调器
swift
// ✅ 每个领域使用范围化协调器
class AuthCoordinator { }      // 登录、注册、找回密码
class MainCoordinator { }      // 标签导航、主流程
class SettingsCoordinator { }  // 设置导航树
class OrderCoordinator { }     // 订单流程、历史、详情

Part 6: Pressure Scenarios

第六部分:压力场景

Scenario 1: "Just hardcode the root for now"

场景1:"现在直接硬编码根视图就行"

The Pressure

压力来源

"We only have one flow right now. Just show MainView directly, we'll add auth later."
"我们现在只有一个流程。直接显示MainView就好,以后再加认证。"

Red Flags

危险信号

  • "We'll add X later" → Tech debt that compounds
  • "It's just one flow" → Flows multiply
  • "Keep it simple" → Simplicity now, complexity later
  • "以后再加X" → 会不断累积的技术债务
  • "只是一个流程" → 流程会越来越多
  • "保持简单" → 现在简单,以后复杂

Time Cost Comparison

时间成本对比

OptionInitialWhen Adding AuthTotal
Hardcode MainView0 min2-4 hours refactor2-4 hours
AppStateController30 min30 min add state1 hour
选项初始成本添加认证时的成本总成本
硬编码MainView0分钟2-4小时重构2-4小时
使用AppStateController30分钟30分钟添加状态1小时

Push-Back Script

反驳话术

"The AppStateController pattern takes 30 minutes now. When we add auth later — and we will — it'll take another 30 minutes to add the state. Hardcoding now saves 0 minutes because we'll spend 2-4 hours refactoring when we need auth. Let's invest 30 minutes now."
"AppStateController模式现在只需要30分钟。以后我们添加认证时——肯定会加的——只需要再花30分钟添加状态。现在硬编码看似节省时间,但以后需要认证时要花2-4小时重构。我们现在花30分钟投资一下吧。"

What to Do

解决方案

  1. Create minimal AppStateController with two states:
    swift
    enum AppState {
        case loading
        case ready
    }
  2. When auth is needed, add states:
    swift
    enum AppState {
        case loading
        case unauthenticated  // Added
        case authenticated(User)  // Added
    }
  3. Total effort: 1 hour instead of 4 hours

  1. 创建包含两个状态的最小AppStateController:
    swift
    enum AppState {
        case loading
        case ready
    }
  2. 需要认证时,添加状态:
    swift
    enum AppState {
        case loading
        case unauthenticated  // 新增
        case authenticated(User)  // 新增
    }
  3. 总工作量:1小时,而非4小时

Scenario 2: "We don't need modules yet"

场景2:"我们现在不需要模块"

The Pressure

压力来源

"Let's keep everything in one target. Modules are over-engineering."
"把所有内容放在一个目标里就行。模块是过度设计。"

Decision Framework

决策框架

CodebaseTeamRecommendation
< 5,000 lines1-2 devsSingle target is fine
5,000-20,000 lines3+ devsConsider modules
> 20,000 linesAnyModules essential
代码库规模团队规模建议
< 5000行1-2人单一目标即可
5000-20000行3+人考虑模块化
> 20000行任意规模必须模块化

Push-Back Script

反驳话术

"I agree modules add overhead. Let's use this decision tree: We have [X] lines and [Y] developers. Based on that, we [should/shouldn't] modularize yet. If we hit [threshold], we'll revisit. Sound good?"
"我同意模块会增加开销。我们用这个决策树:我们的代码库有[X]行,团队有[Y]人。基于此,我们[应该/不应该]现在模块化。如果达到[阈值],我们再重新评估。这样可以吗?"

What to Do

解决方案

  1. Check codebase size:
    find . -name "*.swift" | xargs wc -l
  2. If under threshold, document decision and threshold for revisit
  3. If over threshold, identify natural boundaries first

  1. 检查代码库规模:
    find . -name "*.swift" | xargs wc -l
  2. 如果低于阈值,记录决策和重新评估的阈值
  3. 如果高于阈值,先确定自然边界

Scenario 3: "Navigation is too complex to test"

场景3:"导航太复杂,无法测试"

The Pressure

压力来源

"Testing navigation state is too hard. Let's just do manual QA."
"测试导航状态太难了。我们只做手动QA吧。"

Why This Fails

为什么这会失败

  • Navigation bugs are #1 "works on my machine" cause
  • Deep linking requires automated verification
  • State restoration needs regression testing
  • Manual QA misses edge cases
  • 导航bug是"在我机器上正常"的头号原因
  • 深度链接需要自动化验证
  • 状态恢复需要回归测试
  • 手动QA会遗漏边缘情况

Solution: Test the State Machine

解决方案:测试状态机

swift
// ✅ Test navigation state without UI
@Test func testLoginCompletesOnboarding() async {
    let controller = AppStateController()
    controller.transition(to: .unauthenticated)

    // Simulate login
    await controller.handleLogin(user: mockUser)

    // First-time user goes to onboarding
    #expect(controller.state == .onboarding(.welcome))
}

@Test func testDeepLinkWhileUnauthenticated() async {
    let controller = AppStateController()
    controller.transition(to: .unauthenticated)

    // Deep link to order
    let handled = controller.handleDeepLink(URL(string: "app://order/123")!)

    // Should not navigate — requires auth
    #expect(handled == false)
    #expect(controller.state == .unauthenticated)
}
swift
// ✅ 无需UI即可测试导航状态
@Test func testLoginCompletesOnboarding() async {
    let controller = AppStateController()
    controller.transition(to: .unauthenticated)

    // 模拟登录
    await controller.handleLogin(user: mockUser)

    // 首次登录用户进入引导页
    #expect(controller.state == .onboarding(.welcome))
}

@Test func testDeepLinkWhileUnauthenticated() async {
    let controller = AppStateController()
    controller.transition(to: .unauthenticated)

    // 深度链接到订单
    let handled = controller.handleDeepLink(URL(string: "app://order/123")!)

    // 不应导航——需要认证
    #expect(handled == false)
    #expect(controller.state == .unauthenticated)
}

Push-Back Script

反驳话术

"Navigation is complex, which is exactly why we need automated tests. The AppStateController pattern lets us test state transitions without launching the UI. We can verify deep linking, auth flows, and restoration in seconds. Manual QA can't catch all the combinations."

"导航很复杂,这正是我们需要自动化测试的原因。AppStateController模式让我们无需启动UI即可测试状态过渡。我们可以在几秒钟内验证深度链接、认证流程和状态恢复。手动QA无法覆盖所有组合情况。"

Part 7: Code Review Checklist

第七部分:代码审查检查表

App State

应用状态

  • App state is an enum, not booleans
  • All valid states are explicitly defined
  • State transitions are validated in
    isValidTransition
  • Invalid transitions are caught (assertion in debug, logged in prod)
  • State changes are logged for debugging
  • 应用状态是枚举,而非布尔值
  • 所有有效状态都已显式定义
  • 状态过渡在
    isValidTransition
    中验证
  • 无效过渡会被捕获(调试模式断言,生产模式记录)
  • 状态变更会被记录以便调试

Root View

根视图

  • @main delegates to AppStateController
  • No business logic in @main
  • RootView switches on single source of truth
  • Transitions are animated (no flicker)
  • Loading state has minimum display duration
  • @main将逻辑委托给AppStateController
  • @main中无业务逻辑
  • RootView基于单一数据源切换视图
  • 过渡带有动画(无闪烁)
  • 加载状态有最小显示时长

Scene Lifecycle

场景生命周期

  • scenePhase changes handled in one place
  • Session validated on .active (not blindly trusted)
  • Resources released on .background
  • SceneStorage used for tab selection / navigation state
  • Restored state validated before applying
  • scenePhase变更在一处处理
  • 活跃时验证会话(不盲目信任)
  • 后台时释放资源
  • SceneStorage用于标签选择/导航状态
  • 恢复的状态在应用前经过验证

Module Boundaries

模块边界

  • Features have public API protocols
  • No circular dependencies between modules
  • Navigation delegates to coordinators
  • Dependencies injected, not singletons
  • Module decision documented (why split / not split)
  • 功能有公共API协议
  • 模块之间无循环依赖
  • 导航委托给协调器
  • 依赖注入,而非单例
  • 模块化决策已记录(拆分/不拆分的原因)

Testing

测试

  • State transitions tested without UI
  • Invalid transitions tested
  • Deep link handling tested
  • Restoration validation tested

  • 状态过渡无需UI即可测试
  • 已测试无效过渡
  • 已测试深度链接处理
  • 已测试状态恢复验证

Resources

资源

WWDC: 2025-266, 2024-10150, 2023-10149, 2025-256, 2022-10054
Docs: /swiftui/scenephase, /swiftui/scene, /swiftui/scenestorage, /swiftui/windowgroup, /observation/observable()
Skills: axiom-swiftui-architecture, axiom-swiftui-nav, axiom-swift-concurrency
WWDC: 2025-266, 2024-10150, 2023-10149, 2025-256, 2022-10054
文档: /swiftui/scenephase, /swiftui/scene, /swiftui/scenestorage, /swiftui/windowgroup, /observation/observable()
技能: axiom-swiftui-architecture, axiom-swiftui-nav, axiom-swift-concurrency