Swift By Rahul

AppDelegate vs SceneDelegate in Modern iOS

For many years, the AppDelegate was the undisputed central hub for managing an iOS app's lifecycle. It handled everything from launch to termination, push notifications, URL schemes, and even setting up the app's initial UI UIWindow. Then, with iOS 13 and the advent of multi-window support on iPadOS and Mac Catalyst, Apple introduced the SceneDelegate. This addition often creates confusion for developers, especially when working on projects that support older iOS versions or transitioning from legacy codebases.

In this article, we'll demystify the roles of AppDelegate and SceneDelegate, explore why SceneDelegate came into existence, and clarify their responsibilities in modern iOS development. By the end, you'll have a clear understanding of how these two crucial components work together to manage your app's lifecycle and UI.

Evolution of iOS App Lifecycle Management Pre-iOS 13 AppDelegate UIWindow Root ViewController Post-iOS 13 AppDelegate SceneDelegate UIWindow Root ViewController

The AppDelegate: The Original App Lifecycle Manager

Before iOS 13, the AppDelegate class was the single point of entry for all major application-level events. It conformed to the UIApplicationDelegate protocol and was responsible for:

  • App Launch and Termination: Knowing when the app started and ended.
  • State Transitions: Responding to the app moving to and from the background, becoming active, or resigning active status.
  • Core Services Setup: Initializing frameworks like Core Data, setting up push notifications, handling URL schemes, and managing background tasks.
  • UIWindow Management: Creating the UIWindow object and setting its rootViewController.

Here's a look at some common AppDelegate methods you might remember or still encounter in older projects:

import UIKit
import CoreData // Example for app-wide service

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    // Pre-iOS 13, this was used to create the window and set its root view controller.
    // For iOS 13+, this property is typically nil if using SceneDelegate.
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Perform app-wide initialization here
        print("App did finish launching.")
        // Example: Setup Core Data stack, configure analytics, etc.
        setupCoreDataStack()
        return true
    }

    func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state.
        // This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message)
        // or when the user quits the application and it begins the transition to the background state.
        print("App will resign active.")
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers,
        // and store enough application state information to restore your application to its current state in case it is terminated later.
        print("App did enter background.")
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state;
        // here you can undo many of the changes made on entering the background.
        print("App will enter foreground.")
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive.
        // If the application was previously in the background, optionally refresh the user interface.
        print("App did become active.")
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate.
        print("App will terminate.")
    }

    // Example of an app-wide service setup
    private func setupCoreDataStack() {
        // Core Data setup logic here...
        print("Core Data stack initialized.")
    }

    // MARK: - UISceneSession lifecycle (for iOS 13+ only)
    // These methods are crucial for AppDelegate's role in managing scenes.
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        print("AppDelegate: Requesting configuration for new scene session.")
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
        print("AppDelegate: Did discard scene sessions: \(sceneSessions.count)")
    }
}

The Rise of SceneDelegate (iOS 13+): Multi-Window Support

With iOS 13, Apple introduced the concept of "scenes" to support multi-window experiences on iPadOS (and later Mac Catalyst). A scene represents a single instance of your app's user interface, which can be running independently. For example, on an iPad, a user might have two instances of the same app open side-by-side, each with its own UIWindow and view hierarchy. Each of these instances is managed by its own SceneDelegate.

The SceneDelegate conforms to the UIWindowSceneDelegate protocol and is responsible for:

  • Managing a Single UI Window: It holds a reference to the UIWindow object for its specific scene.
  • Setting the Root View Controller: It's where you typically instantiate and assign the rootViewController for that scene's window.
  • Scene-Specific Lifecycle Events: Responding to events related to its particular UI instance, such as when a scene becomes active, enters the background, or is disconnected.
  • State Restoration: Handling the saving and restoring of UI state for its specific scene.
  • Deep Links: Processing URL schemes or user activities that are specific to that scene.

Here's what a typical SceneDelegate looks like:

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow? // This window is specific to *this scene*

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene is the default or only scene for your application.
        guard let windowScene = (scene as? UIWindowScene) else { return }

        print("SceneDelegate: Scene will connect to session.")

        // Create a new UIWindow for this scene
        window = UIWindow(windowScene: windowScene)

        // Instantiate your root view controller
        let rootViewController = ViewController() // Replace with your actual root VC
        window?.rootViewController = rootViewController

        // Make this window the key window and visible
        window?.makeKeyAndVisible()

        // Handle deep links or user activities specific to this scene
        if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
            // Process user activity here
            print("SceneDelegate: Handled user activity: \(userActivity.activityType)")
        }
        if let urlContext = connectionOptions.urlContexts.first {
            // Process URL here
            print("SceneDelegate: Handled URL: \(urlContext.url)")
        }
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Called as the scene is being released by the system.
        // This occurs shortly after the scene enters the background, or when its session is discarded.
        // Release any resources associated with this scene that can be re-created the next time the scene connects.
        // The scene may re-connect later, as its session was not necessarily discarded.
        print("SceneDelegate: Scene did disconnect.")
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
        print("SceneDelegate: Scene did become active.")
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
        print("SceneDelegate: Scene will resign active.")
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
        print("SceneDelegate: Scene will enter foreground.")
    }

    func sceneDidEnterBackground(_ scene: UISene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.
        print("SceneDelegate: Scene did enter background.")
    }
}

The Relationship: AppDelegate and SceneDelegate Together

The introduction of SceneDelegate didn't make AppDelegate obsolete; rather, it redefined its role. Now, AppDelegate is primarily concerned with app-wide lifecycle events and managing scenes, not the UI itself.

Here's how they interact:

┌──────────────────┐
│   App Launch     │
└──────────────────┘
        │
        ▼
┌──────────────────┐
│   AppDelegate    │
│ (App-level life  │
│  & Scene Mgmt)   │
└──────────────────┘
        │
        ▼  (Request for Scene)
┌──────────────────┐
│ UISceneSession   │
│ (One or more)    │
└──────────────────┘
        │
        ▼  (Connect to Session)
┌──────────────────┐
│   SceneDelegate  │
│  (UI-level life  │
│   & Window Mgmt) │
└──────────────────┘
        │
        ▼
┌──────────────────┐
│    UIWindow      │
└──────────────────┘
        │
        ▼
┌──────────────────┐
│ Root ViewController │
└──────────────────┘

AppDelegate's Remaining Responsibilities (App-wide):

  • App Process Lifecycle: application(_:didFinishLaunchingWithOptions:), applicationWillTerminate(_:). These are truly global for the entire application process.
  • Scene Management: The AppDelegate is responsible for providing UISceneConfiguration objects when the system requests a new scene (application(_:configurationForConnectingSceneSession:options:)). It also handles didDiscardSceneSessions, notifying when a scene is removed.
  • Push Notifications Setup: Registering for push notifications and handling device tokens.
  • Core Data Stack: If your app uses Core Data, the persistent container setup often remains in AppDelegate as it's an app-wide resource.
  • App-wide Services: Initializing third-party SDKs that are truly global and not tied to a specific UI instance (e.g., analytics, crash reporting).
  • URL Handling (Fallback/Global): While SceneDelegate is preferred for scene-specific deep links, AppDelegate can still handle global URL schemes or as a fallback.

SceneDelegate's Responsibilities (Scene-specific):

  • UI Window Management: Creating and managing the UIWindow for its specific scene.
  • Root View Controller Setup: Setting the rootViewController for that scene's window.
  • Scene State Transitions: sceneDidBecomeActive(_:), sceneWillResignActive(_:), sceneDidEnterBackground(_:), sceneWillEnterForeground(_:), sceneDidDisconnect(_:). These methods relate to the visibility and activity of that particular UI instance.
  • Scene-Specific Deep Links/User Activities: Handling scene(_:openURLContexts:) or scene(_:continue:) for URLs or user activities directed at that specific UI.
  • State Restoration: Saving and restoring the UI state of a particular scene.

Practical Scenarios

Let's look at how this split of responsibilities manifests in common development tasks.

Initializing the UI

In an iOS 13+ app, the AppDelegate is no longer directly responsible for creating the UIWindow or setting the rootViewController. That's SceneDelegate's job.

AppDelegate.swift (Minimal for UI setup):

import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Provide the configuration for a new scene.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    // Other app-wide lifecycle methods here...
}

SceneDelegate.swift (UI setup):

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }

        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = MainTabBarController() // Your app's main UI entry point
        window?.makeKeyAndVisible()
    }

    // Other scene-specific lifecycle methods here...
}

This separation allows each scene to manage its own UI independently, crucial for multi-window iPad experiences where different windows might display different parts of your app or even the same part with different data.

Handling Deep Links

Deep links (custom URL schemes, universal links, NSUserActivity) can technically be handled in either delegate, but the best practice for iOS 13+ is to handle them in the SceneDelegate if they are meant to navigate or update the UI of a specific scene.

SceneDelegate.swift (Handling a deep link for its scene):

// ... inside SceneDelegate class ...

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    guard let urlContext = URLContexts.first else { return }
    let url = urlContext.url

    print("SceneDelegate: Received URL: \(url.absoluteString)")

    // Example: Parse the URL and navigate within this scene's UI
    if url.host == "products" && url.pathComponents.count > 1 {
        let productID = url.pathComponents[1]
        // Assuming your rootViewController can handle this navigation
        (window?.rootViewController as? MainTabBarController)?.navigateToProduct(with: productID)
    }
}

If a deep link needs to affect all scenes, or if it's a global action not tied to a specific UI, the AppDelegate might still be used, but this is less common with modern scene-based apps.

AppDelegate vs SceneDelegate Responsibilities AppDelegate (App-wide Scope) • App Process Lifecycle • Scene Management (Creation/Discard) • Push Notifications Setup • Core Data Stack / App-wide Services • Global URL Handling (Fallback) • `application(_:didFinishLaunchingWithOptions:)` SceneDelegate (Scene-specific Scope) • `UIWindow` Management • Root View Controller Setup • Scene State Transitions (Active, Background) • Scene-specific Deep Links / User Activities • Scene State Restoration • `scene(_:willConnectTo:options:)`

Summary

The transition from a single AppDelegate to a combined AppDelegate and SceneDelegate model in iOS 13+ marks a significant evolution in how iOS apps manage their lifecycle and UI. While the AppDelegate retains its role as the gatekeeper for the entire application process and scene management, the SceneDelegate has become the dedicated manager for individual UI instances (scenes).

For modern iOS development, especially when targeting iOS 13 and later, remember this crucial distinction: AppDelegate: Handles application-wide events, manages the creation and destruction of scenes, and sets up global services (e.g., push notifications, Core Data stack). SceneDelegate: Manages the lifecycle of a single UIWindowScene, including its UIWindow, rootViewController, and scene-specific events like foreground/background transitions and deep links that affect only that scene.

By understanding and leveraging this separation of concerns, you can build more robust, modular, and multi-window capable iOS applications.

Happy Swifting!