Lesson 04 / 12
The App Lifecycle
Measuring that the scene delegate's handler takes precedence over the app delegate's, and that declared background modes intersect with those the platform permits.
Published Updated
In this lesson
- Orders the state transitions of an app between foreground and background
- Measures that the scene delegate's handler takes precedence over the app delegate's
- Measures that declared background modes intersect with those the platform permits
- Shows that the order of transitions does not change the class of the meeting
Contents
In the previous lesson, what the app asks for met what the user grants, and the result was an intersection. In this lesson the two definitions become two handlers: both the app delegate and the scene delegate can handle the same event. The question is the same again: how are these two definitions resolved when they meet? The resolution examined in this lesson is precedence: one of the two handlers runs and the other does not.
The app lifecycle is the sequence of states an app passes through from its start to its termination. The app may not be running, it may be active in the foreground, it may be in the foreground but not active (for example, when a system alert covers the screen), it may be running in the background, or it may be suspended. Each state transition is an event, and the platform announces this event to the app by calling a handler. Where the handlers are defined is one of two places.
// Meeting.swift — classifies how two definitions are resolved when they meet
struct Meeting {
let label: String
let first: Set<String>
let second: Set<String>
let result: Set<String>?
init(_ label: String, first: Set<String>, second: Set<String>, result: Set<String>?) {
self.label = label
self.first = first
self.second = second
self.result = result
}
var resolution: String {
if first.isEmpty || second.isEmpty
|| first.isSubset(of: second) || second.isSubset(of: first) {
return "out-of-scope"
}
guard let result, !result.isEmpty else { return "rejection" }
if result == first.union(second) { return "union" }
if result == first.intersection(second) { return "intersection" }
if result == first || result == second { return "precedence" }
return "off-rule"
}
static func list(_ values: Set<String>) -> String {
values.isEmpty ? "empty" : values.sorted().joined(separator: ",")
}
func report() {
let outcome = result.map(Meeting.list) ?? "none"
print("\(label): first \(Meeting.list(first)) | second \(Meeting.list(second)) | result \(outcome) -> \(resolution)")
}
}
func summarize(_ meetings: [Meeting]) {
for resolution in ["precedence", "intersection", "union", "rejection"] {
let count = meetings.filter { $0.resolution == resolution }.count
print("\(resolution): \(count)")
}
}
The App Delegate and the Scene Delegate
An app can have more than one window on the screen; each group of windows is managed as a scene, and each scene has its own lifecycle. The app delegate looks after events that concern the whole app: launch, notification registration, system-level alerts. The scene delegate handles the events that belong to that scene: the scene connecting, moving to the foreground, moving to the background. In an app that uses scenes, the foreground and background transitions arrive at the scene delegate; the older app-level handlers for these transitions are no longer called. This rule is the answer to the question of which handler runs when both are defined.
The piece below depends on a platform framework, so it stands as source and no output is claimed for it. Both delegates define a handler for the same transition.
// Source snippet: both delegates can handle the move to the background
import UIKit
final class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidEnterBackground(_ application: UIApplication) {
// app-level handler
}
}
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func sceneDidEnterBackground(_ scene: UIScene) {
// scene-level handler
}
}
The helper below models two rules. The first looks for a handler for an event: it returns the one in the scene delegate if there is one, otherwise the one in the app delegate, and gives no result if neither exists. The second filters the background modes the app declares against those the platform permits. The file has no output.
// Handlers.swift — resolves the event handler and the background modes
func handler(for event: String, scene: [String: String], app: [String: String]) -> Set<String>? {
if let found = scene[event] { return [found] }
if let found = app[event] { return [found] }
return nil
}
func permittedModes(declared: Set<String>, platform: Set<String>) -> Set<String> {
declared.filter { platform.contains($0) }
}
The Same Event, Two Handlers
The measurement below sets up two handlers for the enter-foreground event: one in the app delegate and one in the scene delegate. The app delegate also defines an event that only it handles. handler returns which handler will run.
// main.swift — the scene handler takes precedence over the app handler
let appHandlers = ["enter-foreground": "app.enter-foreground", "enter-background": "app.enter-background"]
let sceneHandlers = ["enter-foreground": "scene.enter-foreground"]
let event = "enter-foreground"
let both = Meeting("handler",
first: [appHandlers[event]!],
second: [sceneHandlers[event]!],
result: handler(for: event, scene: sceneHandlers, app: appHandlers))
both.report()
handler: first app.enter-foreground | second scene.enter-foreground | result scene.enter-foreground -> precedence
The result is the second side, the scene handler: the app handler dropped out. This precedence works as a rule and not as a race; it does not depend on which was written first or which was compiled first. If the scene handler were not defined, the app handler would remain, as in the enter-background event; in that case no meeting is formed, because there is only one definition.
PS10. This measurement is only for the enter-foreground event and a handler defined in both delegates. The model builds this rule of the platform; whether the real system follows the same order for other events (launch, notification registration) was not measured.
Background Modes
The app declares in its property list what it can do in the background: receiving location updates, playing audio, fetching data. The declaration alone is not enough; the platform applies its own conditions to each mode and may not permit every declared mode. The modes that can run are the common part of those declared and those the platform permits.
<key>UIBackgroundModes</key> <array> <string>location</string> <string>audio</string> <string>fetch</string> </array>
The measurement below declares three modes: location, audio, and fetch. The ones the platform permits are audio, fetch, and notification. The mode names are model names, not the real keys in the property list.
// main.swift — declared modes intersect with those the platform permits
let declared: Set = ["location", "audio", "fetch"]
let platform: Set = ["audio", "fetch", "notification"]
let modes = Meeting("background-modes",
first: declared,
second: platform,
result: permittedModes(declared: declared, platform: platform))
modes.report()
background-modes: first audio,fetch,location | second audio,fetch,notification | result audio,fetch -> intersection
location was declared but is not permitted on the platform side, so it dropped out; notification exists on the platform but the app did not declare it, so it dropped out too. The two remaining modes were found on both sides. This meeting has the same form as the permission flow in the previous lesson and gives the same resolution; the difference is that the party that supplies the result is the platform and not a user.
PS11. This measurement is for three declared modes and three permitted modes. A mode depending on additional conditions (for example an authority) does not exist in this model and was not measured.
State Transitions and Expected Work
Apart from the not-running state, an app has four states. In the inactive state the app is in the foreground but receives no events; for example, it stays briefly in this state when a system alert covers the screen or during a transition. In the active state the app is in the foreground and receives events. In the background state the app is not visible on the screen but can run code. In the suspended state the app stays in memory but does not run code.
The work expected from the app differs at each transition. When moving from active to inactive, the app pauses work that would keep the user waiting: animations, game loops, timers. When moving to the background, the app saves unsaved data, releases shared resources, and frees heavy memory; this work must be short, because the platform gives the app only a limited time for it. When returning to the foreground, the app refreshes its interface with current data and resumes the work it paused. While suspended, the app can do nothing; when memory is needed, the platform can terminate a suspended app without notice. For this reason the final save must be finished before the move to the suspended state; the app does not assume that it will be given a chance before it is terminated.
That the order of transitions is not measured is therefore a deliberate limit: in a real system the order matters, and work done in the wrong order can lead to data loss. But the question of this lesson is the meeting and not the order; which handler will be called is determined by the definitions of the two delegates, not by the order.
The Limits of Background Modes
Background modes give the app the right to work in the background, but this right is limited by the purpose of the mode. An app that plays audio can keep playing in the background, but if it stops playing audio it also loses its right to work in the background. An app that receives location updates is woken only when the location changes. In the fetch mode the platform wakes the app at a time of its own choosing, and the app has to finish its work within a short time; the app cannot determine this time.
What these limits have in common is that the right does not come from the declaration. In the previous measurement, the location mode was declared but dropped out because it was not among those the platform permits. Declaring a mode is a request, not a right; to limit unnecessary resource consumption, the platform checks that the mode is actually used and may restrict a mode that is not used. For this reason the app should declare only the modes it really uses and keep its background work in line with the purpose of the mode.
One last limit belongs to the user. The user can turn off background refresh for the app; in that case even a mode that is declared and permitted by the platform does not run. The intersection in the previous measurement is therefore a lower bound: a third party, the user’s setting, can narrow the set further.
Scene Lifetime and Preserving State
The lifecycle of the scene delegate is separate from that of the app. The platform can detach a scene in the background from the system when memory is needed; the scene closes, but the app process may keep running. When the user returns to the scene, the platform reconnects it and this appears as a new connection: the scene delegate receives the connection event, but it has to rebuild the earlier state of the interface itself. For this reason the user-related state of a scene, for example which screen it was on and an input left half finished, should be saved when the scene moves to the background and restored when it reconnects.
This division of work also shows why the precedence in the previous measurement is useful. In an app with multiple windows each scene has its own foreground and background transitions: one can be in the foreground while another is in the background. The app delegate sees this situation as a single event; only the scene delegate knows which scene moved to which state. That the scene handler takes precedence over the app handler is therefore not a preference but a necessity: the right owner of an event is the party that knows the event best.
The app delegate is not disabled entirely. System-level work, for example remote notification registration and the first launch of the app, still belongs to it. The rule draws this boundary: an event that belongs to a scene goes to the scene, and an event that belongs to the app goes to the app. The cases in which both handlers are really defined for the same event are the only cases in which the resolution comes out as precedence; for the remaining events no meeting is formed in the first place.
The Bounding Measurement
When describing the lifecycle, the order of the transitions comes to the front: first to the foreground, then to the background, then to the foreground again. This measurement does not measure the order. What is measured is only the meeting of two definitions, and the meeting is independent of the order in which a transition arrives. To see this, we resolve the same two events twice in reverse order.
// main.swift — the order of transitions does not change the meeting
let appHandlers = ["enter-foreground": "app.enter-foreground", "enter-background": "app.enter-background"]
let sceneHandlers = ["enter-foreground": "scene.enter-foreground", "enter-background": "scene.enter-background"]
let forward = ["enter-foreground", "enter-background"]
let backward = ["enter-background", "enter-foreground"]
func resolved(_ events: [String], scene: [String: String], app: [String: String]) -> Set<String> {
Set(events.compactMap { handler(for: $0, scene: scene, app: app) }.flatMap { $0 })
}
let orders = [
Meeting("order-forward",
first: Set(forward.map { appHandlers[$0]! }),
second: Set(forward.map { sceneHandlers[$0]! }),
result: resolved(forward, scene: sceneHandlers, app: appHandlers)),
Meeting("order-backward",
first: Set(backward.map { appHandlers[$0]! }),
second: Set(backward.map { sceneHandlers[$0]! }),
result: resolved(backward, scene: sceneHandlers, app: appHandlers)),
]
for item in orders {
item.report()
}
order-forward: first app.enter-background,app.enter-foreground | second scene.enter-background,scene.enter-foreground | result scene.enter-background,scene.enter-foreground -> precedence order-backward: first app.enter-background,app.enter-foreground | second scene.enter-background,scene.enter-foreground | result scene.enter-background,scene.enter-foreground -> precedence
The two lines are the same apart from the label: the sets are printed sorted, and the order in which the transitions are resolved does not change the result. Under which conditions and in which order the transitions are triggered in a real system is a separate subject; it does not enter the measurement axis of this course.
PS12. This measurement is for two events and two orders; in both of them the scene delegate handles both events. The mixed case in which the scene handles only one was not measured.
Summary
- The app lifecycle is the sequence of state transitions between foreground and background, and each transition is announced to a handler.
- In an app that uses scenes, the foreground and background transitions arrive at the scene delegate.
- If two handlers are defined for the same event, the scene handler takes precedence over the app handler: the output is precedence.
- Declared background modes intersect with those the platform permits; location dropped out, and audio and fetch remained.
- The order of transitions does not change the class of the meeting; what is measured is only the meeting of two definitions.
- The resolution of this lesson: precedence.
Next Step
The precedence between the scene and the app was a resolution in which one side takes the place of the other. In the next lesson the same resolution is set up between the user and the app: the user’s text size setting takes precedence over the app’s default. We read the interface design guidelines with this in mind.
To keep your progress and take notes, Log in
My notes
Log in to take notes.