Whether you come from the world of UIKit (AppDelegate and SceneDelegate) or are starting from scratch with SwiftUI, understanding the Application Lifecycle is the most important foundation for building robust apps.
The lifecycle dictates how your application is born, how it lives while the user interacts with it, and how it dies (or gets suspended) when the user leaves it. In SwiftUI, Apple has unified this process, moving away from imperative delegation toward a declarative and reactive approach.
In this tutorial, we will break down every phase of the app lifecycle in SwiftUI in the modern Apple ecosystem, covering the critical differences between iPhone, Mac, and Apple Watch.
1. The Evolution: From Delegation to Declaration
Before SwiftUI 2.0, the entry point for an iOS app was the AppDelegate.swift file. This file was a “catch-all” where we handled initial configuration, push notifications, and background states.
With the arrival of the App protocol in SwiftUI, Apple introduced a new entry point:
@main
struct MySuperApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}What is happening here?
@main: This annotation tells the compiler: “This is where it all begins.” It replaces the oldmain.swiftfile or the@UIApplicationMainannotation.AppProtocol: Your struct conforms to theAppprotocol. Its only requirement is a computed propertybodythat returns aScene.SceneandWindowGroup: An application does not contain views directly; it contains Scenes. A scene is a container for the user interface.WindowGroupis the most common scene, capable of handling multiple windows automatically (crucial on iPadOS and macOS).
2. The Vital Signs Monitor: scenePhase
In the old UIKit, we listened for methods like applicationDidEnterBackground. In SwiftUI, the system “injects” the current state into us via an environment variable called scenePhase.
This is the most powerful tool in your arsenal. It allows your app to react to state changes in real-time.
Basic Implementation
To observe the lifecycle, you need to read the environment variable in your App struct:
import SwiftUI
@main
struct LifecycleApp: App {
// 1. Inject the environment variable
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}
// 2. Listen for changes
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .active:
print("🟢 App is active and responding")
case .inactive:
print("🟡 App is visible but not interactive")
case .background:
print("🔴 App is in the background")
@unknown default:
print("⚪️ Unknown future state")
}
}
}
}3. Deep Dive into States
Understanding when each state triggers is vital to avoid data loss bugs or excessive battery drain.
A. Active
The “happy” state. Your application is in the foreground, occupies the screen (or a window), and is receiving user events (touches, clicks, keyboard input).
- What to do here:
- Resume animations.
- Start timers.
- Request location or gyroscope updates.
- Fetch fresh data from the network if necessary.
B. Inactive
This is a transitional or “limbo” state. The application remains visible on the screen, but it is not receiving touch events.
- When does this happen?
- iOS: When you pull down the Notification Center or open the Control Center.
- iOS: When an incoming call appears or a high-priority system alert pops up (like a FaceID prompt).
- iPadOS: In multitasking, when the user is resizing windows.
- macOS: When the window does not have the main focus.
- What to do here:
- Pause games or heavy logic.
- Blur sensitive content (like in banking apps) for visual security.
C. Background
The user has closed the app (swiped up) or switched to another application. The interface is not visible.
Critical Note: At this point, you have very little time (seconds) before the system suspends your code execution to save battery.
- What to do here (Quickly!):
- Save data: Persist changes to CoreData, UserDefaults, or files. Never wait for the app to terminate to save.
- Cancel network tasks: If you are downloading a large image that isn’t vital, cancel it.
- Clean up resources: Release memory if possible.
4. Platform Differences: The Devil is in the Details
Although SwiftUI tries to be universal, user behavior changes drastically between a watch, a phone, and a computer.
iOS and iPadOS
The behavior is the standard described above. However, remember that on iPadOS (with Stage Manager), a user can have multiple scenes of your app open.
- Challenge: One window might be
.activewhile another is.background.scenePhasetracks the scene, not the entire application.
macOS
The lifecycle on Mac is more complex due to the nature of windows and the menu bar.
- Window Close vs. App Close: On Mac, closing the last window (red X button) does not always close the application. The app keeps running in the Dock.
NSApplicationDelegateAdaptor: You will often need this to handle top menus or specific Dock behaviors.
To force the app to die when closing the last window (common in simple tools), you don’t use scenePhase, but rather a method in the delegate or window logic, though SwiftUI is improving this.
watchOS
The Apple Watch is aggressive with battery.
- Frontmost App: Apps on watchOS switch to
inactivevery quickly when the user lowers their wrist. - Always On Display: If your app supports “Always On,” the lifecycle has nuances. The app might appear active but be in a low refresh mode (1Hz).
- Snapshotting: The system takes a “snapshot” of your UI before sending it to the background to use in the Dock. Ensure the UI looks correct before transitioning to
background.
5. The Bridge to the Past: AppDelegate in SwiftUI
Sometimes, the pure SwiftUI lifecycle isn’t enough. Common use cases:
- Firebase configuration at startup.
- Handling Push Notifications (token registration).
- Handling complex Deep Links.
For this, SwiftUI offers adaptors to inject the old delegate.
In iOS (UIApplicationDelegateAdaptor)
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("🚀 AppDelegate: App has launched (legacy code/libs)")
// Configure Firebase here
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// Handle push tokens
}
}
@main
struct MyHybridApp: App {
// Connect the delegate
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Warning: SwiftUI manages the creation of the Window, so do not attempt to create a UIWindow manually inside the AppDelegate unless you know exactly why you are doing it.
6. Architecture: Where do I put the logic?
A common beginner mistake is placing save logic directly inside the struct App or inside the View. This breaks the principle of separation of concerns.
The MVVM Approach (Model-View-ViewModel)
Ideally, your ViewModel or a Service should handle the reaction to state changes.
Example of Clean Architecture:
class DataManager: ObservableObject {
func saveData() {
print("💾 Saving data to disk...")
}
func refreshData() {
print("🔄 Refreshing data from API...")
}
}
struct ContentView: View {
@Environment(\.scenePhase) var scenePhase
@StateObject var dataManager = DataManager()
var body: some View {
TaskListView(manager: dataManager)
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .active:
// On return, refresh in case of cloud changes
dataManager.refreshData()
case .background:
// On exit, save immediately
dataManager.saveData()
default: break
}
}
}
}Why in the View and not in App?
Sometimes it is better to put the .onChange in the root view (ContentView) instead of the App file. This is because @StateObject lives within the view hierarchy. If your data manager is a StateObject instantiated in the view, it is easier to access it from there.
7. Advanced Use Cases and Gotchas
A. Background Tasks
If you need more time than the few seconds the background state gives you (for example, to finish uploading a file), you need to use the BackgroundTasks framework. The SwiftUI lifecycle does not keep you alive indefinitely on its own.
B. Initialization (init)
The init() of your App struct runs before almost anything else.
- Good: Configuring light dependency injection.
- Bad: Heavy or asynchronous tasks. The UI does not exist yet.
C. Scene Persistence (SceneStorage)
SwiftUI offers @SceneStorage, which is like UserDefaults but specific to each window. If you have two windows of the same app open on iPad, and you kill the app, upon restoration @SceneStorage will remember which tab you had open in each window individually. This is an integral part of the state restoration lifecycle.
Conclusion
The lifecycle in SwiftUI has drastically simplified how we think about our application states. We no longer have to worry about scattered delegate methods; we now have a reactive and centralized state flow.
Survival Summary:
- Use
@mainand theAppprotocol. - Watch
@Environment(\.scenePhase)to detect changes. - Use
.activeto start engines and.backgroundto brake and save. - Don’t be afraid to use
UIApplicationDelegateAdaptorif you need legacy libraries.
Mastering scenePhase is the difference between an app that drains battery and loses data, and an app that feels native, snappy, and reliable on any Apple platform.
If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.