App Lifecycle, Background & Push
Irbisa · cheatsheetSeptember 13, 2026

App Lifecycle, Background & Push

Cold launch, scene phases, background modes, BGTaskScheduler, APNs, silent push, universal links

Middle Developer16 itemscompressed for a skim
  1. 01

    Analytics says sessions doubled after a release nobody shipped analytics changes in — which lifecycle callback is the counter hooked to, and which one should it be?

    Easy

    A counter wired to sceneDidBecomeActive fires again after every Face ID prompt, Control Center swipe and app-switcher peek, so it is counting interruptions; the transition you want is sceneWillEnterForeground.

    final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
      var window: UIWindow?
    
      // Cold launch only — the process was not alive before this
      func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
                 options: UIScene.ConnectionOptions) {
    …
  2. 02

    A user comes back two hours later and the app starts from the launch screen with the draft gone — what killed it, and did you get a callback?

    Easy

    Almost certainly jetsam: the kernel reclaimed your suspended process under memory pressure, and a suspended app gets no callback at all, so anything not written down by sceneDidEnterBackground was lost before the user ever came back.

    final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
      // The last guaranteed moment. There is no callback after suspension.
      func sceneDidEnterBackground(_ scene: UIScene) {
        try? draftStore.write(editor.currentDraft)   // survives a jetsam kill
        UserDefaults.standard.set(router.path, forKey: "lastRoute")
    …
  3. 03

    You call beginBackgroundTask to finish an upload after the user leaves — how much time do you actually get, and what happens if nothing ever ends it?

    Easy

    You get seconds, not minutes, and an identifier that is never ended gets the process killed by the watchdog with exception code 0x8badf00d — which users experience as "the app crashes when I switch away".

    // WRONG: ends the task only on the happy path, and only from a callback
    func uploadOnBackground() {
      let id = UIApplication.shared.beginBackgroundTask(withName: "upload")
      api.upload(draft) { _ in
        UIApplication.shared.endBackgroundTask(id)   // never runs if the request stalls
      }
    …
  4. 04

    Product wants the app to keep syncing after the user closes it, so someone adds the audio background mode to keep the process alive — what do you tell them?

    Medium

    That it is a guaranteed rejection under App Review guideline 2.5.4: a background mode may only be used for the purpose it names, and declaring audio without playing audible content the user asked for is the textbook example Apple cites.

    <!-- Info.plist — declare only what the app genuinely does -->
    <key>UIBackgroundModes</key>
    <array>
      <!-- Required for BGAppRefreshTask (short content refreshes) -->
      <string>fetch</string>
      <!-- Required for BGProcessingTask (longer, charger-friendly work) -->
    …
  5. 05

    A nightly sync is registered with BGTaskScheduler and users report it has never once run — walk me through what you check?

    Medium

    Check the three things that silently disable it before you blame the system: registration that happens after didFinishLaunching has returned, an identifier missing from BGTaskSchedulerPermittedIdentifiers, and a handler that never submits the next request.

    // 1. Register BEFORE didFinishLaunching returns, once per identifier
    func application(_ app: UIApplication,
                     didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
      BGTaskScheduler.shared.register(
        forTaskWithIdentifier: "com.example.app.nightly-sync",
        using: nil                                   // nil = a system-provided queue
    …
  6. 06

    How do you watch your BGProcessingTask handler actually run today, instead of leaving a device on a charger overnight and hoping?

    Medium

    Pause the debugger once the request has been submitted and launch the task by hand from LLDB — _simulateLaunchForTaskWithIdentifier: calls your registered launch handler immediately.

    import BackgroundTasks
    import os
    
    private let logger = Logger(subsystem: "com.example.app", category: "bgtask")
    
    func handleSync(_ task: BGProcessingTask) {
    …
  7. 07

    The push token is uploaded once at sign-up, and months later a slice of users silently stops receiving notifications — what is wrong with that flow?

    Medium

    A device token is neither an identity nor permanent — it changes on reinstall, on restore to a new device, when the system invalidates it, and between the sandbox and production APNs environments — so it has to be re-uploaded on every launch, not once.

    func application(_ app: UIApplication,
                     didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
      // Every launch, not just the first one — the token may have changed
      UIApplication.shared.registerForRemoteNotifications()
    
      // Alert permission is a separate ask; a silent push does not need it
    …
  8. 08

    Your backend fires a content-available push on every data change and the app never wakes up — where do you start looking?

    Medium

    Start with the headers, not the payload: a background push must be sent with apns-push-type: background and apns-priority: 5, and APNs rejects priority 10 on a background push with BadPriority before it ever reaches the device.

    // The payload and headers the server must send:
    //
    //   :path          /3/device/<token>
    //   apns-push-type background      <- required; alert here means no background wake
    //   apns-priority  5               <- 10 is rejected with BadPriority
    //   apns-topic     com.example.app
    …
  9. 09

    A push must arrive already decrypted and with the sender's avatar attached — which extension does that work, and what does its budget force on you?

    Medium

    The notification service extension is the only place that can rewrite an incoming payload before iOS shows it, so decryption and attachment downloads live there; the content extension only draws custom UI after the user expands a notification whose text is already decided.

    import UserNotifications
    
    final class NotificationService: UNNotificationServiceExtension {
      private var handler: ((UNNotificationContent) -> Void)?
      private var bestAttempt: UNMutableNotificationContent?
    …
  10. 10

    A message push arrives while the user is already looking at that chat — what does your notification delegate do, and where do the Reply and Mute buttons come from?

    Medium

    Nothing is shown while the app is frontmost unless userNotificationCenter(_:willPresent:withCompletionHandler:) says so, and the buttons come from a UNNotificationCategory you register at launch and name in the payload's category key.

    // 1. Register categories once, at launch — before any notification can arrive.
    func application(
      _ app: UIApplication,
      didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
      let reply = UNTextInputNotificationAction(
    …
  11. 11

    Product wants notifications working on day one with no permission popup — what does provisional authorisation give you, and where do those notifications land?

    Medium

    Adding .provisional to the options makes requestAuthorization return true without ever showing a prompt, in exchange for quiet delivery: the notifications go straight into Notification Center with no banner, no sound and no lock-screen alert.

    import UserNotifications
    
    let center = UNUserNotificationCenter.current()
    
    // Day one: no prompt at all. Notifications are delivered quietly to Notification Center.
    let granted = try await center.requestAuthorization(
    …
  12. 12

    A universal link opens Safari instead of your app on a tester's phone — what do you check, and in what order?

    Medium

    Start at the association file: https://<domain>/.well-known/apple-app-site-association must return JSON over HTTPS with no redirect, no query string and no authentication, and it must list your TEAMID.bundle.id against a path that matches the link.

    // The file, served at https://example.com/.well-known/apple-app-site-association
    // Content-Type: application/json, HTTPS, no redirect, no query, no auth.
    //
    // {
    //   "applinks": {
    //     "details": [{
    …
  13. 13

    An iPad user keeps three windows of your app open and the system reclaims two overnight — what does each window need to rebuild itself from nothing?

    Medium

    One NSUserActivity per scene, holding identifiers rather than objects — that is the only thing the system preserves for a disconnected scene, and everything that was on screen has to be reconstructible from it.

    // UIKit: one activity per scene, carrying ids only.
    final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
      private let router = Router()
    
      func scene(_ scene: UIScene,
                 willConnectTo session: UISceneSession,
    …
  14. 14

    Your launch dashboard shows cold launches taking four minutes, and the process started long before the user tapped anything — what is going on?

    Hard

    The system is prewarming your app: since iOS 15 it starts the process ahead of a predicted launch, runs dyld, the Objective-C runtime and your static initialisers, then stops just short of UIApplicationMain and leaves the process sitting there — so process start time can be minutes before the tap.

    import OSLog
    
    // WRONG: the clock starts when the process starts, which may be a prewarm minutes early.
    let processStart = CFAbsoluteTimeGetCurrent()          // file-scope global: runs at prewarm
    final class Analytics {
      static let shared = Analytics()
    …
  15. 15

    A crash log shows your app touching UIKit with no window, minutes after the user last used it — how did it get launched, and what must that path avoid?

    Hard

    The system relaunched it straight into the background — a silent push, a BGTaskScheduler task, a finished background URLSession, a location or Bluetooth event — and the very same application(_:didFinishLaunchingWithOptions:) runs, only with applicationState == .background and, usually, no scene ever connected.

    func application(
      _ app: UIApplication,
      didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
      // Same method, no user. Everything below the guard is for a real, visible launch.
      let isBackgroundLaunch = app.applicationState == .background
    …
  16. 16

    The user swipes your app out of the app switcher — after that, which pushes, background tasks and location wake-ups still reach it, and which never will?

    Hard

    A swipe in the app switcher marks the app as user-terminated, and from then until the user opens it again or the device reboots, iOS refuses to relaunch it for almost every background trigger.

    // Scheduled before the swipe. The request survives; nothing will launch you to run it.
    func scheduleRefresh() {
      let request = BGAppRefreshTaskRequest(identifier: "com.example.app.refresh")
      request.earliestBeginDate = .now.addingTimeInterval(15 * 60)
      try? BGTaskScheduler.shared.submit(request)   // silently never runs after a force-quit
    }
    …