All articles
iOS17 min read

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…

K
Karan Pal
Author
Image generated by AI
Image generated by AI

🧭 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?

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:

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:

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:

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:

🔧 Common Use Cases for scenePhase

🧠 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

📦 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
⚠️ 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

📚 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()
    }
}

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:

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 appDelegate

This 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:

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")
    }
⚠️ 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:

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:

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! 🚀

● The newsletter

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