How to distinguish a foreground cold launch from a background task cold launch?

I'm trying to determine why my UIKit application was launched, specifically whether it was launched by the user or by the system to execute a background task.

I recently added a BGProcessingTask to update data overnight. Consider this scenario:

  1. The user backgrounds the app at 10 AM.
  2. A background task is scheduled with earliestBeginDate set to 4 AM the following day.
  3. During those 18 hours, iOS terminates the app's process to reclaim memory.
  4. iOS later decides to execute the scheduled background task and launches a new app process in the background.

My understanding is that this results in a cold launch, including a call to application(_:didFinishLaunchingWithOptions:) and the initialization performed there.

This matters because some of our startup initialization triggers network requests that are unnecessary when the app is launched only to perform a background task.

Questions

1. Can iOS relaunch a previously terminated app to execute a scheduled BGProcessingTask?

I'm referring specifically to termination by the system (for example, memory pressure), not the user force-quitting the application.

2. Is there a reliable way to distinguish a foreground cold launch from a background task cold launch inside application(_:didFinishLaunchingWithOptions:)?

I tried checking UIApplication.shared.applicationState, but in my testing it was .background during didFinishLaunchingWithOptions in both scenarios, including when the user launched the app by tapping its icon.

Is there another documented API or launch context that reliably identifies why the process was started? I'm aware that iOS provides launch options for certain launch scenarios, but I'm specifically interested in distinguishing a background task-triggered cold launch from a user-initiated foreground cold launch.

3. If this distinction cannot be made reliably during didFinishLaunchingWithOptions, what is the earliest lifecycle callback where I can determine that the app is entering the foreground?

For example, should foreground-specific initialization be deferred until scene(_:willConnectTo:options:), sceneWillEnterForeground(_:), or another callback?

I'm specifically looking for a documented and reliable lifecycle signal, rather than heuristics or arbitrary delays.

The goal is to avoid unnecessary startup initialization and network requests when iOS launches the application solely to execute a background task.

Before I answer the questions, I should mention that launchOptions is practically dead. Details below:

1. Can iOS relaunch a previously terminated app to execute a scheduled BGProcessingTask?

Yes. BGProcessingTaskRequest is documented as "a request to launch your app in the background", so a cold launch straight into the background is the expected path. That is also why every launch handler has to be registered before the app finishes launching.

2. Is there a reliable way to distinguish a foreground cold launch from a background task cold launch inside application(_:didFinishLaunchingWithOptions:)?

Not in application(_:didFinishLaunchingWithOptions:). Not anymore! The launch options would be the obvious place, but they are gone for good. In an app that supports scenes, launchOptions is nil; the parameter's documentation says so. Also, apps built with the iOS 27 SDK must adopt the scene-based life cycle or they fail to launch. And, every launch options key is now deprecated.

What you saw with applicationState is documented too. In a scene-based app it takes the value of the most active scene, and "a scene-based app launches in the background state". No scene exists yet during didFinishLaunchingWithOptions, so it reads .background for every launch.

3. If this distinction cannot be made reliably during didFinishLaunchingWithOptions, what is the earliest lifecycle callback where I can determine that the app is entering the foreground?

Split the work by where it belongs. Keep didFinishLaunchingWithOptions to what every launch needs, including the task registration, which has to happen there. Move the foreground-only startup, network requests included, to the scene delegate. sceneWillEnterForeground(_:) is the documented signal: UIKit calls it before a scene moves to the foreground, and it is always followed by sceneDidBecomeActive(_:). It runs on every return to the foreground, not only the first, so guard one-time work with a flag:

func sceneWillEnterForeground(_ scene: UIScene) {
    // Called only when a scene is about to become visible.
    startForegroundWorkIfNeeded()
}

What used to arrive in launchOptions now arrives in the scene's connectionOptions, or through other APIs for the background cases. Apple documents the replacement for each key on its own page under UIApplication.LaunchOptionsKey.

I have a note at 7strikes.dev that puts all 14 launch options and all 14 connection options side by side, which is easier to read than going through the pages one at a time.


S.Beck -- 7strikes.dev

Adding a few notes to expand on S. Beck's excellent reply:

  1. Can iOS relaunch a previously terminated app to execute a scheduled BGProcessingTask?

…

...not the user force-quitting the application.

It can do it for force quits as well, as described here:

  1. Is there a reliable way to distinguish a foreground cold launch from a background task cold launch inside application(_:didFinishLaunchingWithOptions:)?

Not in application(_:didFinishLaunchingWithOptions:). Not anymore!

...or for a pretty long while. Even before we introduced scenes, the system had "accumulated" a significant number of cases which would cause the app to launch directly into the background. Many of them weren't really documented, which meant trying to anticipate/avoid them wasn't really possible.

  1. If this distinction cannot be made reliably during didFinishLaunchingWithOptions, what is the earliest lifecycle callback where I can determine that the app is entering the foreground?

The advice above is excellent, but there was an additional point I wanted to make that's tied to this idea:

This matters because some of our startup initialization triggers network requests that are unnecessary when the app is launched only to perform a background task.

The basic problem here is that once you choose to skip this kind of work, the next question immediately becomes when you're going to do the work you just skipped. You may have been launched into the background for a processing task, but there's NO guarantee that you'll be terminated again, which means the next time your app wakes up is very likely to be when it's entering the foreground.

In other words, putting the work in didFinishLaunchingWithOptions just means you're juggling the work in two places instead of just one. Finally, on S. Beck's comment here:

It runs on every return to the foreground, not only the first, so guard one-time work with a flag:

VERY little network work is in fact "one-time". Your app could be suspended for HOURS at a time, and I have a hard time thinking of network work that looks like this:

  • I'd WANT to do the work again, even though my app was running mere seconds ago (...because the user suspended and then relaunched my app).

BUT

  • I WOULDN'T want to do the work again, even though it's been hours or days since my app was last active (...because my app was suspended in the background for a long time).

The upshot of all this is that you often end up putting this kind of code in willEnterForeground, so you're better off just relying on it alone.

Also, as an aside, these kinds of issues are a big part of why launch options went away:

The launch options would be the obvious place, but they are gone for good. ... And, every launch options key is now deprecated.

That is, the dynamics of app launching and suspension meant that every daemon that triggered a launch ended up needing code to:

  1. Set the launch option if it happened to be launching the app.

  2. A separate mechanism to deliver "whatever" it needed to pass to the app if the app was already running.

This worked ok when we didn't do much background launching, and lack of memory meant that apps tended to die frequently. It didn't really work at all once background launches proliferated and increasing memory meant we kept more apps suspended. For many apps, it’s entirely possible that they NEVER used launch options, simply because their apps’ usage patterns meant that they were never hitting the "right" launch paths.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

How to distinguish a foreground cold launch from a background task cold launch?
 
 
Q