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 Ask |
Why 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:
- Explicit β An enum, not multiple booleans
- Validated β Transitions are checked and logged
- Centralized β One source of truth
- Observable β Views react to state changes
The Boolean Soup Problem
class AppState {
var isLoading = true
var isLoggedIn = false
var hasCompletedOnboarding = false
var hasError = false
var user: User?
}
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
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)
}
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
}
}
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