Appendix A. Lifetime Events

This chapter has been revised for Early Release. It reflects iOS 14, Xcode 12, and Swift 5.3. But screenshots have not been retaken; they still show the Xcode 11 / iOS 13 interface.

The fundamental events that notify you of stages in the lifetime of your app as a whole, giving your code an opportunity to run in response, are extraordinarily important. This appendix is devoted to a survey of them, along with some typical scenarios in which they will arrive.

Application States

In the very early days of iOS — before iOS 4 — the lifetime of an app was extremely simple: either it was running or it wasn’t. The user tapped your app’s icon in the home screen, and your app was launched and began to run. The user used your app for a while. Eventually, the user clicked the Home button and your app was terminated — it was no longer running. The user had quit your app. Launch, run, quit: that was the entire life cycle of an app. If the user decided to use your app again, the whole cycle started over.

The reason for this simplicity was that an iOS device, with its slow processor and its almost brutal paucity of memory and other resources, compensated for its own shortcomings by a simple rule: it could run only one app at a time. While your app was running, it occupied not only the entire screen but the vast majority of the device’s resources, leaving room only for the system and some hidden built-in processes; it had, in effect, sole and complete control of the device.

Starting in iOS 4, that changed. Apple devised an ingenious architecture whereby, despite the device’s limited resources, more than one app could run simultaneously — sort of. The Home button changed its meaning and its effect upon your app. In iOS 4 and later, when the user clicks the Home button to leave your app, your app does not die; technically, the Home button does not terminate your app. Instead, when your app occupies the screen, it is in the foreground (or frontmost); then, when some other app occupies the screen, your app is backgrounded and suspended.

Suspension means that your app is essentially freeze-dried; its process still exists, but it isn’t actively running, and it isn’t getting any events — though notifications can be stored by the system for later delivery in case your app comes to the front once again. And because it isn’t running, it isn’t using so much of the device’s precious resources. In particular, it’s not using the CPU, and some memory may be freed up by clearing the backing store of layers. Later, when the user returns to your app after having left it to use some other app for a while, your app is found in the very same state as when the user left it. The app was not terminated; it simply stopped and froze, and waited in suspended animation. Returning to your app no longer means that your app is launched, but merely that it is resumed.

All of this is not to say, however, that your app can’t be terminated. It can be — though not by the user clicking the Home button. The user might switch off the device; that will certainly terminate your app. And a savvy user might force-terminate your app from the app switcher. The most common scenario, however, is that the system quietly kills your app while it is suspended. This undermines the app’s ability to resume; when the user returns to your app, it will have to launch from scratch, just as in the pre–iOS 4 days. The death of your app under these circumstances is rather like that of the scientists killed by HAL 9000 in 2001: A Space Odyssey — they went to sleep expecting to wake up later, but instead their life-support systems were turned off while they slept. The iOS system’s reasons for killing your app are not quite as paranoid as HAL’s, but they do have a certain Darwinian ruthlessness: your app, while suspended, continues to occupy a chunk of the device’s memory, and the system needs to reclaim that memory so some other app can use it. This forcible ejection of your suspended app from the land of the living is technically known as jetsam.

(I’m simplifying, of course. The reasons why the system kills your app are more complicated than just the amount of memory it uses. An interesting WWDC 2020 video called “Why is my app getting killed?” goes into more detail. Its most important message is that jetsam is not a bug. You should do all you can to make your app a good background citizen, and the video makes some suggestions about that, but even after you’ve done all you can, being killed in the background because the system needs memory is normal and you should expect and plan for it.)

Over time, successive iOS systems have complicated the picture:

  • Some apps can be backgrounded without being suspended. This is a special privilege, accorded in order that your app may perform a limited range of highly focused activities. An app that is playing music or tracking the device’s location when it goes into the background may be permitted to continue doing so in the background. (See Chapters 15 and 22.)

  • An app that has been suspended can be woken briefly, remaining in the background, in order to receive and respond to a message — in order to be told, for instance, that the user has crossed a geofence, or that a background download has completed. (See Chapters 22 and 24.)

  • There is also an intermediate state in which your app can find itself, where it is neither frontmost nor backgrounded. This happens, for instance, when the user summons the control center or notification center in front of your app. In such situations, your app may be inactive without actually being backgrounded.

  • A modern iPad that does iPad multitasking (Chapter 10) is capable of running two apps at once: they are both active (in the foreground) at the same time.

So your app’s code can be running even though the app is not frontmost. If your code needs to know the app’s state in this regard, it can ask the shared UIApplication object for its applicationState (UIApplication.State), which will be one of these:

  • .active

  • .inactive

  • .background

With scene support, things are more complicated if your app supports multiple windows on the iPad. One window can go into the background while another window remains active in the foreground. To describe these possibilities, UIScene has an activationState (UIScene.ActivationState), which will be one of these:

  • .unattached

  • .foregroundActive

  • .foregroundInactive

  • .background

The app’s own state is then considered to be the highest state of any of its scenes. If any of its scenes is in the .foregroundActive state, the app is in the .active state. If all of its scenes are in the .background state, the app is in the .background state. Just as an app can be killed in the background to save memory, a window scene can be disconnected and disposed of in the background to save memory, leaving only its session as a placeholder (see Chapter 10).

Delegate Events

When your app launches, the UIApplicationMain function creates its one and only UIApplication instance as the shared application object, along with the app delegate, which adopts the UIApplicationDelegate protocol. With the addition of scene support, UIApplicationMain also creates a scene and a scene delegate, which adopts the UIWindowSceneDelegate protocol. (See “How an App Launches”.) The runtime then proceeds to report lifetime events to these delegates through calls to the methods declared in those protocols. Other objects can also register to receive some of these events as notifications.

If your app has scene support, some of the app delegate lifetime events, such as applicationDidBecomeActive(_:) and applicationDidEnterBackground(_:), are suppressed. Corresponding messages are sent to the scene delegate instead. In this discussion, I’ll assume that you’ve compiled your app against iOS 14 and that it does have scene support. If you want to know what the app delegate messages are when there is no scene support, consult an earlier edition of this book.

Tip

App delegate methods start with the word application; scene delegate methods start with the word scene.

The suite of basic lifetime events that may be sent to your app delegate and scene delegate is surprisingly limited and considerably less informative than one might have hoped:

application(_:didFinishLaunchingWithOptions:)

The app has started up from scratch. You’ll typically perform initializations here. But in an app with scene support, these initializations should not involve views or view controllers; that’s the job of the scene delegate.

scene(_:willConnectTo:options:)

The scene is being created or reconnected. Perhaps the app has started up from scratch; perhaps a new window has been created in a multiple window app; perhaps this scene was disconnected from its session and is being connected again. Regardless, this is the place to prepare your window’s root view controller if needed. If an app doesn’t have a main storyboard, or is ignoring the main storyboard at launch time, this is the place to ensure that the scene has a window, to set the window’s root view controller, and to show the window (“App Without a Storyboard”). If the app is launching because of an incoming quick action or URL, this is your chance to respond to it (Chapters 14, 23, and 24).

sceneWillEnterForeground(_:)

The scene is coming to the front. This message is sent both when the scene was previously in the background and when the app is launched from scratch. That’s interesting, because in an app without scene support, applicationWillEnterForeground(_:) is not sent on launch to the app delegate. Always followed by sceneDidBecomeActive(_:).

sceneDidBecomeActive(_:)

The scene is now well and truly frontmost. Received after sceneWillEnterForeground(_:). Also received after the end of any situation that caused the scene delegate to receive sceneWillResignActive(_:).

sceneWillResignActive(_:)

The scene is entering a situation where it is neither frontmost nor backgrounded; it will be inactive. Perhaps something has blocked the interface — the user has summoned the notification center, or a system alert has appeared, or there’s an incoming phone call. Whatever the cause, the scene delegate will receive sceneDidBecomeActive(_:) when this situation ends.

Alternatively, the scene may be about to go into the background (and will then probably be suspended); in that case, this event was purely transient, and sceneDidEnterBackground(_:) will follow almost immediately.

sceneDidEnterBackground(_:)

The scene has been backgrounded. Always preceded by sceneWillResignActive(_:). Your app itself may now be backgrounded, in which case it will probably be suspended; before that happens, you have a little time to finish up last-minute tasks, such as relinquishing unneeded memory (see Chapter 6), and if you need more time for a lengthy task, you can ask for it (see Chapter 25).

applicationWillTerminate(_:)

The application is about to be killed dead. Surprisingly, even though every running app will eventually be terminated, it is quite unlikely that your app will ever receive this event! The reason is that, by the time your app is terminated by the system, it is usually already suspended and incapable of receiving events. (I mentioned an exceptional case in Chapter 15, and I’ll mention some more in the next section.)

Lifetime Scenarios

A glance at some typical scenarios will demonstrate the chief ways in which your delegates will receive lifetime events. I find it helpful to group these scenarios according to the general behavior of the events.

Major State Changes

During very significant state changes, such as launching, being backgrounded, or coming back to the front, the app and scene delegates receive a sequence of events:

The app launches from scratch

Your delegates receive these messages:

  • application(_:didFinishLaunchingWithOptions:)

  • scene(_:willConnectTo:options:)

  • sceneWillEnterForeground(_:)

  • sceneDidBecomeActive(_:)

The user clicks the Home button

If your scene was frontmost, the scene delegate receives these messages:

  • sceneWillResignActive(_:)

  • sceneDidEnterBackground(_:)

The user summons your backgrounded scene to the front

The scene delegate receives these messages:

  • sceneWillEnterForeground(_:)

  • sceneDidBecomeActive(_:)

The screen is locked

If your scene is frontmost, the scene delegate receives these messages:

  • sceneWillResignActive(_:)

  • sceneDidEnterBackground(_:)

The screen is unlocked

If your scene was frontmost, the scene delegate receives these messages:

  • sceneWillEnterForeground(_:)

  • sceneDidBecomeActive(_:)

Paused Inactivity

Certain user actions effectively pause the foreground-to-background sequence in the middle, leaving the scene inactive and capable of being either backgrounded or foregrounded, depending on what the user does next. When the scene becomes active again, it might or might not be coming from a backgrounded state:

The user enters the app switcher

If your scene is frontmost, the scene delegate receives this message:

  • sceneWillResignActive(_:)

The user, in the app switcher, chooses another app

If your scene was frontmost, the scene delegate receives this message:

  • sceneDidEnterBackground(_:)

The user, in the app switcher, chooses your scene

If this scene was frontmost, then it was never backgrounded, so the scene delegate receives just this message:

  • sceneDidBecomeActive(_:)

The user summons the control center or notification center

If your scene is frontmost, the scene delegate receives this message:

  • sceneWillResignActive(_:)

Actually, we get willResignActive as the user starts to pull down the notification center, and then if the user pulls it all the way down, we get didBecomeActive and willResignActive in quick succession. I regard this as unfortunate.

The user dismisses the control center or notification center

If your scene was frontmost, the scene delegate receives this message:

  • sceneDidBecomeActive(_:)

But if the user has summoned the notification center, there’s another possibility: the user might tap a notification alert to switch to that app. In that case, the scene will continue on to the background, and the scene delegate will receive this message:

  • sceneDidEnterBackground(_:)

The user holds down the screen-lock button

The device offers to shut itself off. If your scene is frontmost, the scene delegate receives this message:

  • sceneWillResignActive(_:)

The user, as the device offers to shut itself off, cancels

If your scene was frontmost, the scene delegate receives this message:

  • sceneDidBecomeActive(_:)

The user, as the device offers to shut itself off, accepts

If your scene was frontmost, the app delegate receives this message:

  • applicationWillTerminate(_:)

Transient Inactivity on the iPad

There are certain circumstances where your scene may become inactive and then active again in quick succession. These have mostly to do with multitasking on the iPad. If this happens, the scene delegate may receive these messages:

  • sceneWillResignActive(_:)

  • sceneDidBecomeActive(_:)

The chief case in point is when the user toggles between split sizes. When the scene is resized, it undergoes transient inactivity. If your view controller is notified of a change of size and possibly trait collection, this will happen during the period of transient inactivity, while the scene’s activation state is .foregroundInactive (and the app’s application state will probably be .inactive as well).

Multiple Windows

Here are some special cases that arise in a multiple window app on iPad, when your app manipulates the windows directly:

Your app creates a window

If your app creates a window by calling requestSceneSessionActivation, your scene delegate receives these messages:

  • scene(_:willConnectTo:options:)

  • sceneWillEnterForeground(_:)

  • sceneDidBecomeActive(_:)

Your app closes a window

If your app closes a window by calling requestSceneSessionDestruction, then assuming this is not the last window, your delegates receive these messages:

  • sceneDidEnterBackground(_:)

  • sceneDidDisconnect(_:)

  • application(_:didDiscardSceneSessions:)

Scene Death in the App Switcher

The case where the user enters the app switcher and swipes your scene up to remove it is extraordinarily confusing. What happens seems to depend on the device type and the app architecture:

On an iPhone

If the scene was frontmost, you get these delegate messages:

  • sceneDidEnterBackground

  • applicationWillTerminate(_:)

On an iPad, if the app does not support multiple windows

If the scene was frontmost, you get these delegate messages:

  • sceneDidDisconnect(_:)

  • application(_:didDiscardSceneSessions:)

  • applicationWillTerminate(_:)

(The lack of a sceneDidEnterBackground call here is surprising.)

On an iPad, if the app does support multiple windows

If the scene was frontmost, you get these delegate messages:

  • sceneDidEnterBackground

  • applicationWillTerminate(_:)

But if there’s another window still in existence, then the app might not terminate and you won’t get applicationWillTerminate(_:). In that case, you might get sceneDidDisconnect(_:) and possibly application(_:didDiscardSceneSessions:) later.

If the scene that the user swipes up was not frontmost, you’ll get nothing; this scene already received sceneDidEnterBackground, and you won’t get applicationWillTerminate(_:) now because the app isn’t running.

Lifetime Event Timing

The delegate lifetime messages may be interwoven with the lifetime events received by other objects. View controllers (“View Controller Lifetime Events”) are the primary case in point. There are circumstances where the root view controller may receive its initial lifetime events, such as viewDidLoad and viewWillAppear(_:), before application(_:didFinishLaunchingWithOptions:) (or, with scene support, scene(_:willConnectTo:options:)) has even finished running. That may come as a surprise.

Different systems can also introduce changes in timing. When I started programming iOS, back in the days of iOS 3.2, I noted the opening sequence of events involving the app delegate and the root view controller; they arrived in this order:

  1. application(_:didFinishLaunchingWithOptions:)

  2. viewDidLoad

  3. viewWillAppear(_:)

  4. applicationDidBecomeActive(_:)

  5. viewDidAppear(_:)

Relying on that order, I typically used the root view controller’s viewDidAppear(_:) to register for UIApplication.didBecomeActiveNotification in order to be notified of subsequent activations of the app.

That worked fine for some years. But iOS 8 brought with it a momentous change: the app delegate now received applicationDidBecomeActive(_:) after the root view controller received viewDidAppear(_:), like this:

  1. application(_:didFinishLaunchingWithOptions:)

  2. viewDidLoad

  3. viewWillAppear(_:)

  4. viewDidAppear(_:)

  5. applicationDidBecomeActive(_:)

This was a disaster for many of my apps, because the notification I had just registered for in viewDidAppear(_:) arrived immediately.

Then, in iOS 9, the order returned to what it was in iOS 7 and before — knocking my apps into confusion once again. Then, in iOS 11, the order reverted back to what it was in iOS 8!

In iOS 13, with window scene support, the sequence is like this:

  1. application(_:didFinishLaunchingWithOptions:)

  2. scene(_:willConnectTo:options:)

  3. sceneWillEnterForeground(_:)

  4. viewDidLoad

  5. viewWillAppear(_:)

  6. sceneDidBecomeActive(_:)

  7. viewDidAppear(_:)

But in iOS 14 it changes once again:

  1. application(_:didFinishLaunchingWithOptions:)

  2. scene(_:willConnectTo:options:)

  3. sceneWillEnterForeground(_:)

  4. sceneDidBecomeActive(_:)

  5. viewDidLoad

  6. viewWillAppear(_:)

  7. viewDidAppear(_:)

Such changes from one system version to the next are likely to pose challenges for the longevity and backward compatibility of your app. The moral is that you should not, as I did, rely upon the timing relationship between lifetime events of different objects.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset