As soon as your app goes beyond a single screen, window and scene management becomes real architecture, not boilerplate.
Most SwiftUI apps accidentally:
- misuse
ScenePhase - hard-code navigation per window
- break state when opening multiple windows
- duplicate ViewModels
- fight iPad & macOS behavior
- misunderstand what a scene actually is
This post explains how SwiftUI really manages windows and scenes, and how to architect apps that work correctly on:
- iPhone
- iPad (multi-window)
- macOS
- visionOS
π§ 1. App vs Scene (The Most Important Distinction)
App
- Defines what your app is
- Owns global configuration
- Creates scenes
Scene
- Defines how your app is presented
- Owns window lifecycle
- Can exist multiple times simultaneously
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
RootView()
}
}
}
Rule:
π Your app can have multiple scenes, and each scene can have multiple windows.
πͺ 2. What a WindowGroup Really Does
WindowGroup {
ContentView()
}
This means:
- iOS: multiple app windows (iPad)
- macOS: multiple windows
- visionOS: multiple spatial instances
Each window:
- has its own view hierarchy
- has its own state
- does NOT automatically share ViewModels
β οΈ 3. The Biggest Multi-Window Bug
This is wrong:
@StateObject var vm = GlobalViewModel()
inside a WindowGroup.
Why?
- Each window gets a new instance
- State diverges
- Navigation desyncs
π§± 4. Correct Global State Placement
Global state must live above scenes:
@main
struct MyApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
RootView()
.environmentObject(appState)
}
}
}
Now:
- all windows share state
- navigation is consistent
- data stays in sync
π§ 5. Scene-Local State vs App-Global State
Scene-local:
- navigation stack
- selection
- focus
- scroll position
App-global:
- authentication
- user session
- cache
- feature flags
- deep links
Never mix them.
π 6. ScenePhase Is Per-Scene (Not Global)
@Environment(\.scenePhase) var scenePhase
Each window has its own phase.
That means:
- backgrounding one window β app background
- inactive β destroyed
- active β foreground for all windows
Use this wisely.
π§© 7. Supporting Multiple Scene Types
SwiftUI supports multiple scene roles:
var body: some Scene {
WindowGroup("Main") {
MainView()
}
WindowGroup("Inspector") {
InspectorView()
}
Settings {
SettingsView()
}
}
Use cases:
- inspector panels
- settings windows
- auxiliary tools
- debug overlays
πͺ 8. Opening New Windows Programmatically
@Environment(\.openWindow) var openWindow
Button("Open Details") {
openWindow(id: "details")
}
Define the window:
WindowGroup(id: "details") {
DetailView()
}
This is how real desktop-class SwiftUI apps work.
π§ 9. Window-Scoped Dependency Injection
Each window should get:
- its own navigation state
- shared services
- shared app state
Example:
WindowGroup {
RootView(
router: Router(),
services: services
)
}
But:
- services are shared
- routers are per-window
π§ͺ 10. Testing Multi-Window Behavior
You must test:
- opening multiple windows
- closing windows
- backgrounding one scene
- restoring state
- shared state mutation
Most SwiftUI bugs only appear with two windows open.
π Final Thoughts
SwiftUI scenes are not boilerplate β they are architecture.
Once you understand:
- app vs scene
- window identity
- scene-local vs global state
- multi-window behavior
You can build apps that feel:
- native
- correct
- scalable
Top comments (0)