Introduction

Understanding the iOS app lifecycle is fundamental to building reliable, responsive applications. Your app constantly transitions between states as users switch apps, receive calls, or lock their devices. How you handle these transitions determines whether your app feels polished or frustrating.

iOS apps exist in one of several states, and the system sends notifications when transitioning between them. Responding appropriately to these transitions ensures data is saved, resources are released, and users return to exactly where they left off.

This guide covers the complete iOS app lifecycle, from launch to termination, with practical patterns for handling each state correctly.

App States Overview

iOS apps

App States Overview Infographic move through five primary states:

Not Running

The app is not launched or was terminated by the system. No code executes.

Inactive

The app is in the foreground but not receiving events. This brief transitional state occurs during:

  • App launch
  • Incoming phone calls
  • Control Center/Notification Center display
  • System alerts

Active

The app is in the foreground and receiving events. This is the normal running state where users interact with your app.

Background

The app is in the background but executing code. Apps enter this state when:

  • User switches to another app
  • User presses home button
  • System needs to launch app for background task

Background time is limited. Most apps get approximately 5 seconds of execution time before being suspended.

Suspended

The app is in the background but not executing code. The system keeps the app in memory but may terminate it without notice to reclaim resources.

App Delegate Methods

For apps no

t using scenes (iOS 12 and earlier pattern), AppDelegate handles lifecycle events.

Application Launch

func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    // One-time setup
    configureAnalytics()
    configurePushNotifications()
    setupCoreData()

    // Check launch options for deep links, notifications, etc.
    if let url = launchOptions?[.url] as? URL {
        handleDeepLink(url)
    }

    return true
}

This method is called once per app launch. Perform essential setup here, but keep it fast. Slow launches lead to app termination and poor user experience.

State Transitions

func applicationWillResignActive(_ application: UIApplication) {
    // App about to move from active to inactive
    // Pause ongoing tasks, disable timers
    pauseGame()
    saveCurrentProgress()
}

func applicationDidEnterBackground(_ application: UIApplication) {
    // App now in background
    // Save data, release shared resources
    saveApplicationState()
    releaseSharedResources()

    // Request additional background time if needed
    var backgroundTask: UIBackgroundTaskIdentifier = .invalid
    backgroundTask = application.beginBackgroundTask {
        application.endBackgroundTask(backgroundTask)
        backgroundTask = .invalid
    }

    // Perform critical work
    completeCriticalSave {
        application.endBackgroundTask(backgroundTask)
        backgroundTask = .invalid
    }
}

func applicationWillEnterForeground(_ application: UIApplication) {
    // App about to enter foreground
    // Undo background changes, refresh data
    refreshUserInterface()
    checkForUpdates()
}

func applicationDidBecomeActive(_ application: UIApplication) {
    // App now active
    // Restart paused tasks, refresh UI
    resumeGame()
    updateBadgeCount()
}

func applicationWillTerminate(_ application: UIApplication) {
    // App about to terminate
    // Save data, perform final cleanup
    // Note: Not called if app is suspended first
    saveAllData()
}

Scene-Based Lifecycle (iOS 13+)

iOS 13 introduced scenes, enabling multiple instances of your app’s UI. This is particularly relevant for iPad multitasking.

Scene Delegate

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

    func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        // Scene connecting
        // Create window and root view controller
        guard let windowScene = scene as? UIWindowScene else { return }

        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = MainViewController()
        window?.makeKeyAndVisible()

        // Handle any URLs or user activities
        if let urlContext = connectionOptions.urlContexts.first {
            handleDeepLink(urlContext.url)
        }
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Scene disconnected
        // Release resources specific to this scene
        // Called when system discards scene (different from background)
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Scene now active
        refreshData()
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Scene about to become inactive
        pauseActivities()
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Scene moving to foreground
        prepareInterface()
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Scene now in background
        saveState()
    }
}

App Delegate with Scenes

When using scenes, AppDelegate still handles app-level events:

class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // App-level setup only
        // UI setup moves to SceneDelegate
        configureFrameworks()
        return true
    }

    func application(
        _ application: UIApplication,
        configurationForConnecting connectingSceneSession: UISceneSession,
        options: UIScene.ConnectionOptions
    ) -> UISceneConfiguration {
        // Return scene configuration
        return UISceneConfiguration(
            name: "Default Configuration",
            sessionRole: connectingSceneSession.role
        )
    }

    func application(
        _ application: UIApplication,
        didDiscardSceneSessions sceneSessions: Set<UISceneSession>
    ) {
        // User closed scenes
        // Clean up resources for discarded scenes
        for session in sceneSessions {
            cleanupResources(for: session)
        }
    }
}

Handling Common Scenarios

Saving User Data

Save data at appropriate lifecycle points:

class DataManager {
    static let shared = DataManager()

    private var unsavedChanges = false

    func markDirty() {
        unsavedChanges = true
        scheduleAutoSave()
    }

    private func scheduleAutoSave() {
        // Auto-save after period of inactivity
        NSObject.cancelPreviousPerformRequests(
            withTarget: self,
            selector: #selector(autoSave),
            object: nil
        )
        perform(#selector(autoSave), with: nil, afterDelay: 3.0)
    }

    @objc func autoSave() {
        guard unsavedChanges else { return }
        save()
    }

    func save() {
        // Perform save
        persistToDisk()
        unsavedChanges = false
    }

    func saveIfNeeded() {
        if unsavedChanges {
            save()
        }
    }
}

// In SceneDelegate
func sceneWillResignActive(_ scene: UIScene) {
    DataManager.shared.saveIfNeeded()
}

func sceneDidEnterBackground(_ scene: UIScene) {
    DataManager.shared.save()
}

Pausing and Resuming Activities

Handle media playback, downloads, and other ongoing activities:

class MediaPlayer {
    private var wasPlayingBeforeInterruption = false

    init() {
        setupNotifications()
    }

    private func setupNotifications() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleInterruption),
            name: AVAudioSession.interruptionNotification,
            object: nil
        )

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appWillResignActive),
            name: UIApplication.willResignActiveNotification,
            object: nil
        )

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appDidBecomeActive),
            name: UIApplication.didBecomeActiveNotification,
            object: nil
        )
    }

    @objc private func appWillResignActive() {
        wasPlayingBeforeInterruption = isPlaying
        // Continue playing if background audio enabled
        // Otherwise pause
        if !hasBackgroundAudioCapability {
            pause()
        }
    }

    @objc private func appDidBecomeActive() {
        if wasPlayingBeforeInterruption && !isPlaying {
            play()
        }
    }

    @objc private func handleInterruption(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
              let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
              let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
            return
        }

        switch type {
        case .began:
            wasPlayingBeforeInterruption = isPlaying
            pause()
        case .ended:
            if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
                let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
                if options.contains(.shouldResume) && wasPlayingBeforeInterruption {
                    play()
                }
            }
        @unknown default:
            break
        }
    }
}

Handling Phone Calls and Interruptions

System interruptions affect your app’s state:

class GameViewController: UIViewController {
    private var gameState: GameState = .playing
    private var stateBeforeInterruption: GameState?

    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appWillResignActive),
            name: UIApplication.willResignActiveNotification,
            object: nil
        )

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appDidBecomeActive),
            name: UIApplication.didBecomeActiveNotification,
            object: nil
        )
    }

    @objc private func appWillResignActive() {
        stateBeforeInterruption = gameState
        if gameState == .playing {
            pauseGame()
        }
    }

    @objc private func appDidBecomeActive() {
        // Don't auto-resume - show pause menu
        // Let user choose when to resume
        if stateBeforeInterruption == .playing {
            showPauseMenu()
        }
    }

    private func pauseGame() {
        gameState = .paused
        gameEngine.pause()
        showPauseOverlay()
    }
}

State Restoration

State restoration returns users to exactly where they left off.

Opt-In to State Restoration

// AppDelegate
func application(
    _ application: UIApplication,
    shouldSaveSecureApplicationState coder: NSCoder
) -> Bool {
    return true
}

func application(
    _ application: UIApplication,
    shouldRestoreSecureApplicationState coder: NSCoder
) -> Bool {
    return true
}

Assign Restoration Identifiers

class MainViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        restorationIdentifier = "MainViewController"
        restorationClass = MainViewController.self
    }
}

extension MainViewController: UIViewControllerRestoration {
    static func viewController(
        withRestorationIdentifierPath identifierComponents: [String],
        coder: NSCoder
    ) -> UIViewController? {
        return MainViewController()
    }
}

Encode and Decode State

class DetailViewController: UIViewController {
    var itemID: String?

    override func encodeRestorableState(with coder: NSCoder) {
        super.encodeRestorableState(with: coder)
        coder.encode(itemID, forKey: "itemID")
    }

    override func decodeRestorableState(with coder: NSCoder) {
        super.decodeRestorableState(with: coder)
        itemID = coder.decodeObject(forKey: "itemID") as? String
        if let id = itemID {
            loadItem(with: id)
        }
    }
}

Scene-Based State Restoration

For scene-based apps, use NSUserActivity:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        // Create activity representing current state
        let activity = NSUserActivity(activityType: "com.app.viewingItem")
        activity.userInfo = [
            "itemID": currentItemID,
            "scrollPosition": scrollView.contentOffset.y
        ]
        return activity
    }

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Restore from activity
        if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
            restoreState(from: activity)
        }
    }

    private func restoreState(from activity: NSUserActivity) {
        guard let userInfo = activity.userInfo else { return }

        if let itemID = userInfo["itemID"] as? String {
            navigateToItem(itemID)
        }

        if let scrollPosition = userInfo["scrollPosition"] as? CGFloat {
            scrollView.contentOffset.y = scrollPosition
        }
    }
}

Background Execution

Apps can request limited background execution for specific tasks.

Background Task Completion

Request time to complete tasks when backgrounded:

func sceneDidEnterBackground(_ scene: UIScene) {
    var backgroundTask: UIBackgroundTaskIdentifier = .invalid

    backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "SaveData") {
        // Expiration handler - clean up
        UIApplication.shared.endBackgroundTask(backgroundTask)
        backgroundTask = .invalid
    }

    // Perform work
    DispatchQueue.global().async {
        self.saveAllPendingData()

        DispatchQueue.main.async {
            UIApplication.shared.endBackgroundTask(backgroundTask)
            backgroundTask = .invalid
        }
    }
}

Background Modes

Enable specific background capabilities in your app’s Info.plist:

  • Audio: Continue playing audio
  • Location updates: Receive location changes
  • VoIP: Handle incoming VoIP calls
  • External accessory: Communicate with accessories
  • Bluetooth: Act as Bluetooth accessory
  • Background fetch: Periodically fetch content
  • Remote notifications: Wake for silent push notifications

Background Fetch

Schedule periodic content updates:

// AppDelegate
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    // Set minimum fetch interval
    application.setMinimumBackgroundFetchInterval(
        UIApplication.backgroundFetchIntervalMinimum
    )
    return true
}

func application(
    _ application: UIApplication,
    performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
    // Fetch new data
    DataService.shared.fetchLatestContent { result in
        switch result {
        case .success(let hasNewData):
            completionHandler(hasNewData ? .newData : .noData)
        case .failure:
            completionHandler(.failed)
        }
    }
}

Memory Warnings

Respond to low memory conditions to prevent termination:

class ImageCache {
    private var cache = NSCache<NSString, UIImage>()

    init() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleMemoryWarning),
            name: UIApplication.didReceiveMemoryWarningNotification,
            object: nil
        )
    }

    @objc private func handleMemoryWarning() {
        // Clear cache to free memory
        cache.removeAllObjects()
    }
}

// In view controller
override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()

    // Release any cached data, images, etc.
    imageCache.removeAll()

    // Release anything not currently visible
    if !isViewLoaded || view.window == nil {
        view = nil
    }
}

Best Practices

Launch Time Optimization

Keep launch fast:

  • Defer non-essential setup
  • Load data asynchronously
  • Use launch storyboard for instant UI
  • Profile with Instruments
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    // Essential only
    setupCriticalServices()

    // Defer everything else
    DispatchQueue.main.async {
        self.setupAnalytics()
        self.setupRemoteConfig()
        self.prefetchContent()
    }

    return true
}

Avoid Blocking Main Thread

Long operations during transitions cause poor UX:

func sceneDidEnterBackground(_ scene: UIScene) {
    // Quick synchronous work only
    markSaveNeeded()

    // Heavy work in background
    DispatchQueue.global(qos: .utility).async {
        self.performHeavySave()
    }
}

Test Lifecycle Thoroughly

Simulate various scenarios:

  • Incoming calls during critical operations
  • Low memory conditions
  • App termination and restoration
  • Rapid app switching
  • Background fetch timing

Use Xcode’s debugging tools:

  • Simulate Memory Warning (Debug menu)
  • Background Fetch (Debug menu)
  • Slow Animations to spot transition issues

Conclusion

Proper lifecycle management separates professional apps from amateur ones. Users expect apps to save their work, resume seamlessly, and handle interruptions gracefully.

Key takeaways:

  1. Save data at multiple points, not just termination
  2. Handle interruptions without losing user progress
  3. Implement state restoration for seamless return
  4. Respect background execution limits
  5. Respond appropriately to memory warnings

Test your lifecycle handling thoroughly. The scenarios users encounter are unpredictable, and getting this right builds trust in your app.


Building an iOS app that needs robust lifecycle management? The Awesome Apps team specializes in creating reliable iOS applications that handle every edge case. Contact us to discuss your project.