axiom-app-composition

0
1
Source

Use when structuring app entry points, managing authentication flows, switching root views, handling scene lifecycle, or asking 'how do I structure my @main', 'where does auth state live', 'how do I prevent screen flicker on launch', 'when should I modularize' - app-level composition patterns for iOS 26+

Install

mkdir -p .claude/skills/axiom-app-composition && curl -L -o skill.zip "https://mcp.directory/api/skills/download/6111" && unzip -o skill.zip -d .claude/skills/axiom-app-composition && rm skill.zip

Installs to .claude/skills/axiom-app-composition

About this skill

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)

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

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

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

The Boolean Soup Problem

// ❌ 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

The AppStateController Pattern

Step 1: Define Explicit States

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

@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)
        ])
    }
}

Step 3: Initialize from Storage

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)
        }
    }
}

State Machine Diagram

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

Testing State Machines

@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)
}

The State-as-Bridge Pattern (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.

// ✅ 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

Part 2: Root View Switching Patterns

Core Principle

"The @main entry point should be a thin shell. All logic belongs in Ap


Content truncated.

axiom-swiftui-nav-diag

CharlesWiltgen

Use when debugging navigation not responding, unexpected pops, deep links showing wrong screen, state lost on tab switch or background, crashes in navigationDestination, or any SwiftUI navigation failure - systematic diagnostics with production crisis defense

54

axiom-swiftui-26-ref

CharlesWiltgen

Use when implementing iOS 26 SwiftUI features - covers Liquid Glass design system, performance improvements, @Animatable macro, 3D spatial layout, scene bridging, WebView/WebPage, AttributedString rich text editing, drag and drop enhancements, and visionOS integration for iOS 26+

33

axiom-extensions-widgets-ref

CharlesWiltgen

Use when implementing widgets, Live Activities, Control Center controls, or app extensions - comprehensive API reference for WidgetKit, ActivityKit, App Groups, and extension lifecycle for iOS 14+

13

axiom-ios-build

CharlesWiltgen

Use when ANY iOS build fails, test crashes, Xcode misbehaves, or environment issue occurs before debugging code. Covers build failures, compilation errors, dependency conflicts, simulator problems, environment-first diagnostics.

253

axiom-camera-capture-ref

CharlesWiltgen

Reference — AVCaptureSession, AVCapturePhotoSettings, AVCapturePhotoOutput, RotationCoordinator, photoQualityPrioritization, deferred processing, AVCaptureMovieFileOutput, session presets, capture device APIs

42

axiom-swiftdata

CharlesWiltgen

Use when working with SwiftData - @Model definitions, @Query in SwiftUI, @Relationship macros, ModelContext patterns, CloudKit integration, iOS 26+ features, and Swift 6 concurrency with @MainActor — Apple's native persistence framework

12

You might also like

flutter-development

aj-geddes

Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.

1,6841,428

ui-ux-pro-max

nextlevelbuilder

"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."

1,2621,324

drawio-diagrams-enhanced

jgtolentino

Create professional draw.io (diagrams.net) diagrams in XML format (.drawio files) with integrated PMP/PMBOK methodologies, extensive visual asset libraries, and industry-standard professional templates. Use this skill when users ask to create flowcharts, swimlane diagrams, cross-functional flowcharts, org charts, network diagrams, UML diagrams, BPMN, project management diagrams (WBS, Gantt, PERT, RACI), risk matrices, stakeholder maps, or any other visual diagram in draw.io format. This skill includes access to custom shape libraries for icons, clipart, and professional symbols.

1,5331,147

godot

bfollington

This skill should be used when working on Godot Engine projects. It provides specialized knowledge of Godot's file formats (.gd, .tscn, .tres), architecture patterns (component-based, signal-driven, resource-based), common pitfalls, validation tools, code templates, and CLI workflows. The `godot` command is available for running the game, validating scripts, importing resources, and exporting builds. Use this skill for tasks involving Godot game development, debugging scene/resource files, implementing game systems, or creating new Godot components.

1,355809

nano-banana-pro

garg-aayush

Generate and edit images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Use when the user asks to generate, create, edit, modify, change, alter, or update images. Also use when user references an existing image file and asks to modify it in any way (e.g., "modify this image", "change the background", "replace X with Y"). Supports both text-to-image generation and image-to-image editing with configurable resolution (1K default, 2K, or 4K for high resolution). DO NOT read the image file first - use this skill directly with the --input-image parameter.

1,263727

pdf-to-markdown

aliceisjustplaying

Convert entire PDF documents to clean, structured Markdown for full context loading. Use this skill when the user wants to extract ALL text from a PDF into context (not grep/search), when discussing or analyzing PDF content in full, when the user mentions "load the whole PDF", "bring the PDF into context", "read the entire PDF", or when partial extraction/grepping would miss important context. This is the preferred method for PDF text extraction over page-by-page or grep approaches.

1,481684