SwiftUI Lifecycle in 2025: How SwiftUI & iOS App Lifecycle Really Work (Explained Clearly)
In this article, you’ll learn how SwiftUI apps start, pause, background, and restore state — and how all of it ties back to iOS’s UIKit…

🧭 Introduction
If you’ve recently jumped into SwiftUI (or even if you’ve been swimming in it for a while 🏊♂️), you’ve probably run into this question at least once:
“Wait… when does my app actually start?” Followed shortly by: “Where do I handle things like push setup, analytics, or saving state when the app goes to background?”
In UIKit, we had a big ol’ welcome mat called AppDelegate, and lifecycle events like applicationDidEnterBackground(_:) were our jam. But SwiftUI flipped that script. Now we get the @main App struct, and everything feels... magical. ✨ Until you realize your deep link didn’t trigger, or your app forgets what the user just did when it relaunches. Oof.
That’s why in this guide, we’re going to demystify how SwiftUI’s lifecycle really works in 2025 — from the moment your app launches, to when it goes inactive, backgrounds, and returns like nothing happened. And yes, we’ll peek under the hood to see how UIKit is still secretly pulling some strings. 🤫
Whether you’re a UIKit veteran transitioning into SwiftUI or a beginner trying to understand why your app behaves like a goldfish 🐠 (short memory, easily distracted), this article is for you.
🚪 SwiftUI’s Entry Point: The @main App Struct
Back in the UIKit days, your app’s journey started with UIApplicationMain, and you had a trusty sidekick called AppDelegate to guide your app through launch, backgrounding, and even unexpected interruptions (hello, phone calls 📞).
SwiftUI, however, threw out that old rulebook and gave us something cleaner — meet your new app entry point:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}That’s it. No main.m, no AppDelegate.swift — just your SwiftUI App struct with a Scene.
🧠 So what’s happening here?
@maintells the compiler: “Hey, start running the app from here.”- The
Appprotocol conforms to SwiftUI’s lifecycle system. - Inside
body, you define the scene(s) your app uses. The most common isWindowGroup, which manages a window on iOS, iPadOS, and macOS.
Think of this as SwiftUI’s way of saying: “Here’s what to display when the app launches. I’ll handle the rest.”
But “the rest” includes a lot: initializing services, managing deep links, responding to push notifications, restoring previous state — and none of that is directly obvious in this code. That’s why many devs try to sneak UIApplicationDelegate back in. (Spoiler: you can, and we’ll talk about it later!)
🔥 Pro Tip:
If you need to perform setup logic when your app launches — like initializing Firebase, setting up push notifications, or starting analytics — you can hook into SwiftUI’s lifecycle using .task or .onAppear.
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
setupServices()
}
}
}
func setupServices() {
print("App launched. Setting up services...")
}
}It’s not didFinishLaunchingWithOptions, but it gets the job done.
🧱 WindowGroup, Scene, and Multiple Entry Points
When you declare a WindowGroup inside your SwiftUI App, you're essentially telling the system:
“Hey iOS, I want a new window that displays this view — and manages its own lifecycle.”
For most apps, especially on iPhone and iPad, **WindowGroup** is the default scene type, and it works beautifully… until you want to support multiple windows (👋 macOS) or special sections like Settings or Menu Bar items.
💡 What is a Scene?
A scene in SwiftUI is a container for a part of your app’s UI that the system can manage independently. This means it can:
- Launch separately
- Be suspended or closed independently
- Restore its own state
That sounds like a big deal, because it is. In 2025, this makes SwiftUI apps more scalable and future-proof, especially as Apple pushes multiplatform design.
🧩 Common Scene Types in SwiftUI
Let’s take a quick tour:
✅ WindowGroup
WindowGroup {
ContentView()
}Used for the main user interface — it supports multiple instances on iPad and macOS.
⚙️ Settings
Settings {
SettingsView()
}Great for adding a native preferences window on macOS (and now even supported better in visionOS 👀).
🗂️ DocumentGroup
DocumentGroup(newDocument: MyDocument()) { file in
DocumentView(document: file.$document)
}For apps that handle file-based documents — e.g., text editors, drawing apps.
🍔 MenuBarExtra
MenuBarExtra("MyApp", systemImage: "star") {
MenuView()
}Creates a menu bar utility — kind of like a little floating app that lives in the top bar.
🧠 Why does this matter for lifecycle?
Each scene has its own lifecycle. That means:
- Scene phases can differ across windows.
- Each
WindowGrouporDocumentGroupmight go inactive or background separately. - State restoration happens at the scene level, not just globally.
If you’re building for iPadOS, macOS, or visionOS, you’ll likely need to handle this — especially with multi-window support being emphasized more in recent SDKs.
🔄 Understanding App Phases: Active, Inactive, Background
One of SwiftUI’s most “Swifty” lifecycle features is how it uses reactive state to track where your app is in its lifecycle. Instead of overriding delegate methods like in UIKit, you now observe the app’s phase using the scenePhase environment value.
🔍 Meet @Environment(\.scenePhase)
This little guy tells you whether your app is:
.active– The app is on screen and interactive..inactive– The app is transitioning (e.g., you’re swiping away Control Center)..background– The app is off-screen and not receiving input.
Here’s how you can use it:
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .active:
print("App became active")
case .inactive:
print("App is inactive")
case .background:
print("App moved to background")
saveState()
@unknown default:
print("Unexpected new phase")
}
}
}
}you now have a lifecycle monitor that’s:
- Reactive
- Declarative
- Platform-aware
🔧 Common Use Cases for scenePhase
- Saving app data when entering background
- Refreshing UI when coming back to foreground
- Pausing timers or audio playback
- Tracking app usage for analytics
🧠 Pro Tip:
Always handle the .background phase for cleanup and state saving. iOS might kill your app shortly after entering background if you don't behave like a good memory citizen 🧹.
Now that we’ve covered scene phases, you’re probably wondering:
“If there’s no _didFinishLaunchingWithOptions_, where do I actually run my app setup code?”Great question — and that’s exactly what we’re covering next in:
👉 Where’s My **didFinishLaunching**? SwiftUI’s Replacement for Launch Logic
🧨 Where’s My didFinishLaunching? SwiftUI’s Replacement for Launch Logic
If you’ve come from UIKit, you probably remember the legendary didFinishLaunchingWithOptions. It was your launchpad for everything: setting up Firebase, configuring push notifications, logging that sweet cold start event… ✨
But in SwiftUI?
That method is gone.
Instead, you’re given a much more declarative approach. SwiftUI expects you to initialize things inside your App struct using Swift constructs like .task, .onAppear, and even good ol’ dependency injection.
🔧 Option 1: Use .task in Your Root View
If you just need to fire off setup logic once the UI loads:
WindowGroup {
ContentView()
.task {
await initializeApp()
}
}This runs after the view appears — good for async setup like fetching remote config or setting up analytics.
🔧 Option 2: Use a @MainActor Initializer or Singleton
If you want something that behaves very close to didFinishLaunching, you can inject an app-level manager that sets everything up as soon as the app launches:
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
init() {
AppSetupManager.shared.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}💡 _Yes, you can still sneak UIKit back in using_ **_UIApplicationDelegateAdaptor_**_!_ It’s totally allowed, especially if you’re dealing with things like push notifications, background fetch, or deep linking — all of which still use UIKit under the hood.⚠️ Things to Avoid
- Don’t overdo
.task {}in too many views — it can become a mess to manage. - Don’t assume
@main’sinit()is async-safe — it’s not, so keep it lightweight or defer heavy work. - Avoid assuming SwiftUI will handle everything — it won’t (yet). For many real-world apps, you’ll still need to coordinate UIKit lifecycles in hybrid setups.
📦 State Restoration in SwiftUI (2025 Update)
One of the most underrated but hugely important aspects of the app lifecycle is this:
“When users return to my app, how do I make it look like they never left?” (Without storing everything in _UserDefaults_ like it’s 2014 😅)🧠 The Problem SwiftUI Solves (Now Better in 2025)
Before 2025, SwiftUI had limited tools for restoring lightweight UI state. You had to hack things together or reach for UIKit’s state restoration APIs.
But now? Apple has given us official, structured ways to persist state using property wrappers built into SwiftUI:
🗂️ @SceneStorage – Restore Per-Scene View State
@SceneStorage("selectedTab") private var selectedTab = 0- Stores view-specific state that automatically gets saved and restored by the system.
- Works across app restarts — as long as the scene stays alive.
- Great for things like selected tabs, scroll positions, or draft inputs.
⚠️ It only works _inside views rendered by a_**_Scene_**, like your_WindowGroup_. Using it elsewhere will silently fail.
🧑💻 @AppStorage – UserDefaults on Steroids
@AppStorage("isLoggedIn") private var isLoggedIn = false- Backs your property with
UserDefaults, but SwiftUI automatically observes and syncs the value. - Works across scenes and even across app launches.
- Perfect for user preferences, login flags, theme choices, etc.
📚 Want More?
If you’re wondering when to use @AppStorage vs. @SceneStorage — or when to jump to full-blown SwiftData or Core Data — I’ve broken it all down for you in this guide: 👉 SwiftUI Data Persistence in 2025
**SwiftUI Data Persistence in 2025: SwiftData, Core Data, AppStorage & SceneStorage Explained** _Need to store user settings, session data, or a full-blown object graph in your SwiftUI app? This 2025 guide breaks…_medium.comhttps://medium.com/swift-pal/swiftui-data-persistence-in-2025-swiftdata-core-data-appstorage-scenestorage-explained-f10a012c7c00
🧵 Bonus: ScenePhase + Storage = ✨
Want to be a lifecycle pro? Combine @Environment(\.scenePhase) with @SceneStorage to save when the app backgrounds and restore on launch.
.onChange(of: scenePhase) { phase in
if phase == .background {
saveDraft()
}
}- Use
@SceneStorageto save data from views - Use
@Environment(\.scenePhase)to store additional required information
SwiftUI won’t magically save everything — it gives you the tools to do it. And as of 2025, those tools are a lot less leaky 🔧
🕵️ When SwiftUI Isn’t Enough: Accessing UIKit Lifecycle Events
SwiftUI introduced a beautiful, declarative way to build apps using the @main keyword and the App protocol. It simplifies app structure, promotes reactive patterns, and makes code feel cleaner.
But there are times SwiftUI doesn’t expose certain system-level events directly — like:
- Push notification registration
- Background fetch
- URL scheme handling
- App termination cleanup
So what do you do when SwiftUI doesn’t offer the hooks you need?
🔌 Bridging to UIKit with @UIApplicationDelegateAdaptor
Apple provides an official property wrapper just for this:
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegateThis allows you to use a traditional UIApplicationDelegate implementation inside a SwiftUI-based app:
class AppDelegate: NSObject, UIApplicationDelegate {
func applicationDidEnterBackground(_ application: UIApplication) {
print("App moved to background")
}
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
print("App launched via AppDelegate")
return true
}
}This isn’t a workaround — it’s an official SwiftUI feature, documented by Apple as the way to handle events that aren’t directly exposed by SwiftUI’s App structure.
🧠 When to Use It
Reach for @UIApplicationDelegateAdaptor when:
- You need system-level lifecycle access
- SwiftUI doesn’t offer a lifecycle hook for your use case
- You’re integrating with services like Firebase, background tasks, or deep linking
It’s a clean way to bridge two worlds — and still lets you write most of your app in SwiftUI.
🔄 SwiftUI View Lifecycle: What Happens When Views Appear, Disappear & Update
While we’ve talked about the app’s lifecycle, SwiftUI has its own view lifecycle — and it’s very different from UIKit’s viewDidLoad() or viewWillAppear().
There’s no delegate-based model here. Instead, SwiftUI views are value types (structs), and the system treats them as snapshots of UI state that can be created and destroyed frequently.
So how do you respond to view lifecycle events? Here’s the SwiftUI way:
🟢 1. onAppear & onDisappear
Text("Welcome")
.onAppear {
print("View appeared")
}
.onDisappear {
print("View disappeared")
}onAppear: Called when the view enters the screen or becomes visible.onDisappear: Called when the view leaves the screen.
⚠️ Be aware: these can trigger multiple times due to state changes or transitions — they’re not strictly one-time like UIKit’s _viewDidLoad()_.🟣 2. init() in Views
SwiftUI views are structs, so you can initialize them just like any struct:
struct ProfileView: View {
init() {
print("ProfileView init")
}
var body: some View {
Text("Hello, user!")
}
}But remember: views can be recreated frequently, so don’t put heavy or one-time logic here. Prefer .task or external ViewModel logic for real work.
🔵 3. .task for async lifecycle
Starting with iOS 15+, .task {} is a great place to launch async logic tied to a view’s appearance:
Text("Loading…")
.task {
await viewModel.fetchUserData()
}his runs once when the view appears, and automatically cancels if the view goes away.
⚠️ Lifecycle in SwiftUI ≠ Lifecycle in UIKit
Here’s how SwiftUI’s view lifecycle compares to UIKit — no viewDidLoad, no viewWillAppear, and definitely no nib files:
- UIKit:
**viewDidLoad**→ SwiftUI: Use.task {}oronAppearin your top-level view - UIKit:
**viewWillAppear**→ SwiftUI:onAppear { }— but remember, it can be called multiple times - UIKit:
**viewWillDisappear**→ SwiftUI:onDisappear { } - UIKit: keeping view state alive → SwiftUI: Use
@State,@Binding,@ObservedObject,@StateObject, etc.
SwiftUI flips the lifecycle on its head — it doesn’t want you managing lifecycles manually. Instead, it wants you to react to state changes declaratively. That means fewer bugs, but also a bit of a learning curve.
🧠 Pro Tips & Common Mistakes
You’ve made it this far, which means you’re now wiser than most when it comes to SwiftUI’s lifecycle in 2025. But before you close this tab and go fix your .scenePhase bugs, let’s wrap things up with a few practical tips (and pitfalls to dodge):
✅ Pro Tips
1\. Use **.task** for async launch logic
Don’t block your views with init() overload — use .task {} for Firebase setups, feature flags, or async bootstrapping.
2\. Let **@SceneStorage** do the work
For lightweight view state (e.g. selected tab, text field), you don’t need to manually save anything. Just use @SceneStorage and move on with your life 😌
3\. Use **scenePhase** for side effects, not for persistence
Only use @Environment(\.scenePhase) when you need to trigger logic like syncing, cleanup, or analytics — not for saving basic state that SwiftUI already handles.
4\. Modularize your lifecycle logic
Keep your App struct thin. Push logic into app managers or coordinators — especially if you’re following MVVM or Clean Architecture. 🔗 Related read: _MVVM in SwiftUI_
**MVVM in SwiftUI Explained: Build Scalable & Testable Apps with Clean Architecture** _Let’s unravel the mystery of MVVM and learn how to structure your SwiftUI apps with clean, testable architecture…_medium.comhttps://medium.com/swift-pal/mvvm-in-swiftui-explained-build-scalable-testable-apps-with-clean-architecture-2b36443ebfca
5\. Don’t fear **UIApplicationDelegateAdaptor**
It’s officially supported and super useful when you need access to low-level system events. Use it responsibly — and document it well for future you.
❌ Common Mistakes
1\. Expecting SwiftUI to restore everything automatically
SwiftUI helps with lightweight restoration — but for full session recovery (e.g., draft states, login sessions), you’ll still need to manage data persistence yourself. 🔗 Read this for help: _SwiftUI Data Persistence in 2025_
**SwiftUI Data Persistence in 2025: SwiftData, Core Data, AppStorage & SceneStorage Explained** _Need to store user settings, session data, or a full-blown object graph in your SwiftUI app? This 2025 guide breaks…_medium.comhttps://medium.com/swift-pal/swiftui-data-persistence-in-2025-swiftdata-core-data-appstorage-scenestorage-explained-f10a012c7c00
2\. Overusing **.task** or **.onAppear** in too many views
This leads to hard-to-track bugs and inconsistent behavior. Try to centralize setup logic in your root-level views or app entry point.
3\. Forgetting scene boundaries
@SceneStorage won’t work outside a scene context — and scenePhase reflects the lifecycle of a specific scene, not the whole app.
4\. Thinking UIKit is “gone”
SwiftUI is modern, yes — but UIKit isn’t deprecated. Many critical features still flow through UIKit lifecycle channels.
🎬 Conclusion
SwiftUI’s lifecycle may seem magical at first, but behind the clean syntax is a thoughtfully layered system that mixes declarative design with system-level control.
In 2025, you have more tools than ever:
- Modern app structure via
@main - Reactive lifecycle observation with
scenePhase - Built-in state persistence via
@SceneStorageand@AppStorage - And access to UIKit lifecycle events when you need them
Mastering this blend of SwiftUI and UIKit gives you the confidence to build scalable, reactive, and robust apps — without guessing what’s going on behind the scenes.
🎉 Enjoyed this article? Your support means the world to me!
💬 Drop a comment below! I love hearing about your experiences and answering questions
🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal
💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal
Happy coding! 🚀
New articles, straight to your inbox.
No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.
Keep reading
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read