🌙
☀️ Dark
PART 4

iOS Foundations

App lifecycle, scenes, states, permissions, and system services.

Beginner 45 min read
Volume 4: iOS Foundations

iOS Application Foundations & Lifecycle

Learning Objectives

By the end of this chapter, you will understand exactly how an iOS application is structured, how it is managed by the operating system, and how to interact with system-level services. You will be able to build apps that gracefully handle lifecycle changes, deep links, background execution, permissions, and system notifications.

Prerequisites

You must understand basic Swift syntax, classes, structs, closures, and the differences between value and reference types.

Why Does This Exist?

An iOS application is not a standalone executable running in isolation with infinite resources. It is a guest in a strictly controlled, battery-constrained operating system. The OS needs a standardized way to launch apps, put them to sleep, request user permissions, manage localized assets, and route external events (like notifications or URL deep links) to the right place. The iOS project structure, lifecycle APIs, and system services exist to create a predictable contract between your code and the operating system.

The Problem Before the Solution

In the early days of computing, applications assumed they owned the entire machine. They loaded all their assets into memory, ran in an infinite loop, and stayed open until the user explicitly quit them. If they wanted to access a file or the network, they just did it without asking.

Why the Old Approach Breaks

On a mobile device, allowing an app to run indefinitely drains the battery and consumes limited RAM. If apps don't share resources properly, the active app crashes. Furthermore, allowing apps unrestricted access to the camera, microphone, or network creates massive security and privacy risks. The "desktop" approach breaks completely on mobile devices. We need strict application states, permission boundaries, and structured asset management.

History

iPhone OS 1.0 didn't even allow third-party apps. When the SDK launched in iOS 2, apps were completely single-tasking; pressing the Home button instantly killed the app. In iOS 4, Apple introduced background multitasking and the concept of "Suspended" apps. Later, iOS 13 split the app lifecycle (AppDelegate) from the UI lifecycle (SceneDelegate) to support multiple windows on iPad. Today, SwiftUI’s App protocol wraps this complexity, but the underlying mechanisms remain the same.

Mental Model

Think of your iOS app not as a continuous movie, but as a stage play.

The Operating System is the Director. Your App is the Lead Actor. Your application does not get to decide when the curtain goes up or when the lights go out. The Director yells "Action!" (Foreground), and you perform. The Director yells "Freeze!" (Background), and you must stop everything immediately and save your state. If you try to keep moving while the Director says freeze, you are fired (terminated).

Now remove the analogy. Here is what Swift/iOS actually does: The OS manages a finite state machine for your process. When your app transitions from Active to Background, your threads are literally paused by the kernel. You are given a few seconds to save user data before your process is suspended in RAM.

Internal Working

When the user taps your app icon:

  1. The system reads your Info.plist to find your entry point and required configurations.
  2. The OS forks a process and loads your compiled binary and linked libraries into memory (pre-main).
  3. UIApplicationMain (or SwiftUI's @main macro) executes, setting up the UIApplication singleton.
  4. The Main RunLoop starts, processing hardware events (touches) and system events (lifecycle).
  5. Your application delegates (or Scene phases) are notified that the app is launching, then moving to the foreground, then becoming active.

Visual Explanation

User Taps Icon
      |
[ System Kernel ] -> Fork Process -> Read Info.plist
      |
[ Process Launch ] -> load dynamic libraries (pre-main time)
      |
[ @main / main.swift ]
      |
UIApplication created
      |
AppDelegate: didFinishLaunching
      |
SceneDelegate: willConnectTo (UI is created here)
      |
SceneDelegate: sceneDidBecomeActive
      |
[ Event Loop ] <--- (Touches, Network, URL Schemes, Notifications)
      |
(User swipes home)
      |
SceneDelegate: sceneDidEnterBackground
      |
[ Process Suspended in RAM ]
    

Syntax

@main
struct EngineerApp: App {
    @Environment(\.scenePhase) var scenePhase

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { oldPhase, newPhase in
            if newPhase == .background {
                // Save data immediately
            }
        }
    }
}
    

@main: The compiler directive indicating the entry point of the binary.

scenePhase: The environment value that tracks the lifecycle state of the current scene (Active, Inactive, Background).

Tiny Example

Here is the smallest useful implementation of handling an external deep link and monitoring the app's lifecycle.

import SwiftUI

@main
struct LifecycleApp: App {
    @Environment(\.scenePhase) var scenePhase
    @State private var deepLinkData: String = "None"

    var body: some Scene {
        WindowGroup {
            Text("Deep link received: \(deepLinkData)")
                .onOpenURL { url in
                    deepLinkData = url.absoluteString
                }
        }
        .onChange(of: scenePhase) { phase in
            switch phase {
            case .active:
                print("App is active. Refresh UI.")
            case .inactive:
                print("App is inactive. Pause games/media.")
            case .background:
                print("App is in background. Save state.")
            @unknown default:
                break
            }
        }
    }
}
    

Walkthrough

When this app launches, it registers the WindowGroup. The scenePhase transitions from background to inactive, and finally to active. If the user receives a phone call, the phase goes to inactive—the app is still visible, but touch events are routed to the phone call banner. When the user swipes home, the phase goes to background. At this point, the OS grants a few seconds to execute code before freezing the process. The onOpenURL modifier is a SwiftUI abstraction that registers with the system to intercept any URL scheme routed to our app (e.g., engineerapp://profile/123), automatically passing the URL into the closure.

Break It

Let's intentionally break background execution by starting a long-running network request when the app goes into the background, without telling the system.

.onChange(of: scenePhase) { phase in
    if phase == .background {
        // BUG: The system will suspend the app before this finishes!
        Task {
            let data = await downloadLargeFile()
            saveToDisk(data)
        }
    }
}
    

Because we didn't request a background task, the OS suspends the process mid-download. When the user returns hours later, the download has failed or the app might have been terminated entirely.

Debug It

To fix this, we must explicitly ask the OS for a background task using UIApplication.shared.beginBackgroundTask.

.onChange(of: scenePhase) { phase in
    if phase == .background {
        let taskId = UIApplication.shared.beginBackgroundTask {
            // This closure is called if time expires before we finish.
        }
        
        Task {
            let data = await downloadLargeFile()
            saveToDisk(data)
            UIApplication.shared.endBackgroundTask(taskId)
        }
    }
}
    

Using the Xcode Debugger, you can simulate a background transition. If you forget to call endBackgroundTask, Xcode will show a CPU warning, and eventually the OS watchdog will terminate your app for abusing background time.

Mini Project (20-30 min)

URL Scheme & Permission Logger (20 minutes). Build an app that requests notification permissions on launch, declares a custom URL scheme in its Info.plist, and maintains an in-memory list of every time the app moves between foreground and background.

View Reference Solution

Use UNUserNotificationCenter.current().requestAuthorization in the init of your App struct. Set up a deep link in the target info (e.g. logger://). Use @Environment(\.scenePhase) to track state changes and append strings like "Foreground at 10:00 AM" to an @State array, displaying it in a List.

Bigger Project (1-2 hours)

A production app requires a robust Deep Link Router. When a push notification is tapped, or a web link is opened, the app must parse the URL, authenticate the user if necessary, and navigate to the correct screen.

Production Implementation

enum AppRoute {
    case profile(id: String)
    case settings
    case unknown
}

class DeepLinkRouter: ObservableObject {
    @Published var currentRoute: AppRoute?
    
    func handle(url: URL) {
        // e.g., myapp://profile/12345
        guard url.scheme == "myapp" else { return }
        
        let path = url.host ?? ""
        if path == "profile", let id = url.pathComponents.last {
            currentRoute = .profile(id: id)
        } else if path == "settings" {
            currentRoute = .settings
        } else {
            currentRoute = .unknown
        }
    }
}

// In SwiftUI App:
@StateObject var router = DeepLinkRouter()

WindowGroup {
    ContentView()
        .environmentObject(router)
        .onOpenURL { url in
            router.handle(url: url)
        }
}
    

This implementation decouples URL parsing from the UI. The router manages state, and the UI reacts to changes in currentRoute.

Production Usage

In real-world applications (like Spotify, Twitter, or banking apps), deep links are used heavily for email campaigns and OAuth login callbacks. Furthermore, assets are organized into .xcassets and localized using Localizable.xcstrings so the system can dynamically serve high-resolution images and correct languages without manual if/else checks in code.

Performance

App startup time is a critical performance metric. Doing heavy work (like database migrations or synchronous network calls) in didFinishLaunching or init of your App struct blocks the main thread, leading to a delayed first frame. The OS watchdog will kill your app if it takes more than 20 seconds to launch. Always defer non-essential initialization to background threads or load it lazily.

Best Practices

  • Never assume your app will remain in memory. The OS can and will terminate suspended apps to reclaim RAM.
  • Always use beginBackgroundTask if you must finish a critical task like saving to disk.
  • Never hardcode localized strings; always use the localization system so assets can be managed properly.
  • Keep the Info.plist clean and strictly request only the permissions (camera, location) your app actually needs, providing clear usage descriptions.

Interview Questions

Easy: What is the difference between an app being in the Background and being Suspended?

In the Background state, your code is still actively executing. In the Suspended state, your process remains in RAM but is frozen; no code is executing and no CPU cycles are used.

Medium: If a user taps a deep link while your app is completely terminated (cold start), how does it differ from when the app is already in the background?

In a cold start, the system must first launch the process, call application delegates, and build the UI hierarchy before delivering the URL payload. In a warm start, the URL is simply delivered to the running application via onOpenURL or the SceneDelegate.

Hard: How would you safely handle an OAuth callback URL scheme that contains a secure token, preventing malicious apps from hijacking it?

Custom URL schemes (like myapp://) are not secure because any app can claim them. Universal Links (HTTPS links) are required for secure deep linking. They rely on an Apple App Site Association (AASA) file hosted on your secure server, proving that the app and the domain belong to the same entity. The system checks this before routing the link.

Bigger Project (1-2 hours)

The Challenge: A user receives a push notification. They tap it, but the app was fully killed in the background due to memory pressure. You need to route them to a specific chat screen, but the chat screen requires the heavy Core Data stack to be initialized first.

Hint: Do you block the launch, or launch to a loading state and then route?

View Reference Solution

Never block the main thread on launch. Launch the app into a loading or splash screen immediately. Pass the notification payload to your Router. The Router observes the state of the Core Data stack (perhaps a publisher or an async task). Once Core Data is ready, the Router transitions the UI state from loading to chatScreen(id).

Revision Sheet

• Lifecycle: Not Running → Inactive → Active → Inactive → Background → Suspended → Terminated.

• State Handling: Use @Environment(\.scenePhase) in SwiftUI to observe transitions and save data.

• Deep Links: Use Universal Links for security; intercept URLs with onOpenURL.

• Permissions: Always requested dynamically with rationale strings in Info.plist.

• Backgrounding: The OS will freeze you. Request explicit background tasks for saving crucial data.

Connections

Understanding the app lifecycle connects directly to Architecture (Part 8), as you must structure your repositories to survive being suspended, and Persistence (Part 10), as background transitions are the exact moment you flush in-memory data to Core Data or SwiftData.

Mini Project (20-30 min)

Observe the app lifecycle by implementing NotificationCenter observers for background state transitions.

View Solution
swift

// Observing App Lifecycle
class AppLifecycleObserver {
    init() {
        NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
    }
    @objc func appDidEnterBackground() {
        print("App moved to background - save state!")
    }
}
                

Bigger Project (1-2 hours)

Construct a multi-window capable SceneDelegate to programmatically handle UIWindow creation without storyboards.

View Solution
swift

// Handling SceneDelegate States
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 }
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIViewController()
        self.window = window
        window.makeKeyAndVisible()
    }
}
                

Interview Questions

1. (Easy) What is the main difference between AppDelegate and SceneDelegate?
View Answer
AppDelegate handles global application-level events (like push notifications and app launch), while SceneDelegate manages the lifecycle of individual UI instances or windows (allowing multi-window support on iPadOS).
2. (Medium) Explain the application lifecycle states (Active, Inactive, Background, Suspended).
View Answer
Active: The app is running in the foreground and receiving events. Inactive: The app is running but not receiving events (e.g., during a phone call). Background: The app is executing code in the background. Suspended: The app is frozen in memory by the OS and executes no code.
3. (Hard) How do you handle background tasks when the app is suspended?
View Answer
You must request extra background execution time from the OS using `beginBackgroundTask(expirationHandler:)` in UIKit before the app suspends. For longer operations, you should rely on `BGTaskScheduler` to wake up the app at system-determined intervals.