App Composition
Patterns for structuring an app at the top level: the @main entry point, app-wide states such as loading and signed in, switching the root view between them, scene lifecycle, multiple windows, and document-based apps.
When to Use
Use this skill when you're:
- Structuring your
@mainentry 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 the system terminates your app
- Deciding when to split your app into feature modules
- Coordinating multiple windows on iPad or visionOS
- Building a document-based app with the
@Observabledocument model (iOS 27)
Core principle: Apps have discrete states. Model them with an enum, not scattered booleans.
Example Prompts
Questions you can ask your assistant that will draw from this skill:
- "How do I switch between login and main screens?"
- "My app flickers when switching from splash to main."
- "Where should auth state live?"
- "How do I handle my app going to the background?"
- "How do I restore state after my app is killed?"
- "When should I split my app into modules?"
- "How do I build a document-based app with the new
@Observabledocument model?" (iOS 27) - "How do I show a New Window button only when the device supports multiple windows?"
What This Skill Provides
App-Level State Machines
- The
AppStateenum pattern (loading, unauthenticated, onboarding, authenticated, error) in place of "boolean soup" such asisLoading,isLoggedIn, andhasError - Validated transitions in a central
AppStateController, testable without UI - The state-as-bridge pattern: synchronous state changes drive the UI, and async work lives in the model
Root View Switching
- A thin
@mainthat hands off to a root view - Animated transitions and a minimum loading duration that prevent flicker on launch
- Coordinator integration at the root
Scene Lifecycle and Windows
scenePhasehandling, and why the value differs when read from a view or from the app- Session validation on activation and resource cleanup in the background
SceneStoragestate restoration, with validation of restored state- Multi-window coordination on iPad and visionOS, including when to show a New Window button
Document-Based Apps (iOS 27)
- An
@Observabledocument class (ReadableDocument,WritableDocument) with async reading and writing and progress reporting - The new
DocumentGroupinitializers for viewers and editors, and when to keepFileDocument
Modularization
- A decision tree for when to split
- Module boundaries, dependency injection, and navigation between modules
Migration and Review
- Incremental SwiftUI adoption in a UIKit app (
UIHostingController,UIViewControllerRepresentable, anAppDelegatealongside@main) - Anti-patterns, pressure scenarios, and a code review checklist
Key Patterns
AppState Enum Pattern
// ❌ 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?
// ✅ Explicit states — compiler prevents invalid combinations
enum AppState: Equatable {
case loading
case unauthenticated
case onboarding(OnboardingStep)
case authenticated(User)
case error(AppError)
}
@Observable
@MainActor
class AppStateController {
private(set) var state: AppState = .loading
func transition(to newState: AppState) {
// isValidTransition(from:to:) lists every legal transition
guard isValidTransition(from: state, to: newState) else {
assertionFailure("Invalid transition: \(state) → \(newState)")
return
}
state = newState
}
}Thin @main and Root View
@main
struct MyApp: App {
@State private var appState = AppStateController()
var body: some Scene {
WindowGroup {
RootView()
.environment(appState)
.task { await appState.initialize() }
}
}
}
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)
}
}Scene Lifecycle
Read at the app level, scenePhase is an aggregate: .active means any scene is active, and .background means every scene is in the background.
@main
struct MyApp: App {
@State private var appState = AppStateController()
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
RootView()
.environment(appState)
}
.onChange(of: scenePhase) { _, newPhase in
switch newPhase {
case .active:
Task { await appState.validateSession() }
case .inactive:
appState.prepareForBackground()
case .background:
appState.releaseResources()
@unknown default:
break
}
}
}
}Documentation Scope
This page documents the app-composition skill (in the axiom-design suite), which your assistant draws on when you structure @main, manage authentication flows, or handle scene lifecycle.
- For UIKit scene delegates and the iOS 27 scene-lifecycle requirement, see uikit-modernization
- For the Mac document-app shell (File menu, document tabs, and restoration), see windows
Related
- swiftui-architecture – Feature-level architecture (MVVM, TCA, coordinators) beneath the app level
- swiftui-nav – Navigation inside each app state, including deep links and state restoration
- swift-concurrency – Async patterns behind session validation and loading
- swift-testing – Moving app logic into a Swift package so
swift testruns it without a simulator - iphone-duo – Why the outer display can't open windows, and how to gate the New Window button
Resources
WWDC: 2022-10054, 2023-10149, 2024-10150, 2025-256, 2025-266
Docs: /swiftui/scenephase, /swiftui/scene, /swiftui/scenestorage, /swiftui/windowgroup, /observation/observable(), /swiftui/documentgroup, /swiftui/readabledocument, /swiftui/writabledocument