Solving Prewarming Challenges with Live Activities and Protected Data in iOS
With the release of iOS 16, Apple introduced Live Activities—a feature that allows apps to display real-time updates on the Lock Screen and in the Dynamic Island (on supported devices). Live Activities enable users to stay informed about ongoing events, such as sports scores, delivery statuses, or, in the case with the OK app, the status of their paid parking sessions, without constantly opening the app.
While implementing Live Activities, we uncovered a significant issue related to app prewarming that can affect UserDefaults. Specifically, if a Live Activity was active when turning off the phone, when booting back on, the system–we assume–may try to prewarm the app before the device is unlocked in order to update the Live Activity, leading to potential errors or missing values in these components. This blog post delves into our discovery, the technical details, and how we addressed the issue.
Background: User Challenges and the Breakthrough in Identifying the Bug
We began receiving reports from users experiencing unexpected logouts and missing payment methods after restarting their devices. These incidents were sporadic and lacked consistent reproduction steps, making it challenging to identify the root cause. We added non-fatal logs using Crashlytics to gather more data, but the insights were minimal.
Despite attempting various fixes—such as reducing the size of objects stored in the Keychain and reviewing our data storage mechanisms—the issue persisted without a clear pattern.
The Breakthrough
The turning point came when a team member discovered that the issue could be reliably reproduced by:
- Starting a Live Activity within the app.
- Rebooting the device.
This revelation allowed us to investigate the root cause more effectively.
Debugging
To dive deeper into the issue, we developed a small demo app designed to replicate the problem in a controlled environment. The app workflow is straightforward:
-
Onboarding: Upon first launch, the app presents an onboarding screen. Completing the onboarding sets a boolean flag in UserDefaults to indicate that the user has finished this step.
-
Triggering a Live Activity: After onboarding, the app starts a Live Activity to simulate our real-world use case.
-
Rebooting the Device: We then reboot the device to observe the app's behavior upon restart.
After Unlocking: Upon opening the app, the onboarding screen is presented again, which should not happen since the completion flag was previously set in UserDefaults.
This behavior confirmed our suspicion; the app is being launched and is executing significant portions of its code while the device is still locked. More importantly, the boolean flag in UserDefaults that indicates onboarding completion is being read as false, suggesting that UserDefaults is returning default or nil values during prewarming.
Understanding Prewarming: How It Affects Your App
App Prewarming
In iOS 15 and later, the system may prewarm apps to improve performance and responsiveness. According to Apple's documentation:
Prewarming executes an app’s launch sequence up until, but not including, when main() calls UIApplicationMain(_:_:_:_:). This provides the system with an opportunity to build and cache any low-level structures it requires in anticipation of a full launch.
Key Points About Prewarming from Apple:
- Prewarming can occur before the device is unlocked after a reboot.
- It executes code before UIApplicationMain, meaning that initialisers and static variables may be evaluated.
- The app's application(_:didFinishLaunchingWithOptions:) method may be called.
How Live Activities Trigger Prewarming
When a Live Activity is active during a device shutdown, the operating system prewarms the app almost immediately after a reboot – even before the user unlocks the device for the first time. This is likely because the system aims to update the Live Activity displayed on the Lock Screen as soon as possible. Consequently, the app is launched in the background, and significant portions of its code may be executed while the device remains locked.
The Cause
During prewarming, our app attempts to access data from UserDefaults, Keychain, and files. However, due to iOS's data protection mechanisms:
Although UserDefaults uses NSFileProtectionCompleteUntilFirstUserAuthentication by default – making it accessible after the first device unlock – it may return nil or default values during prewarming before the first unlock. This happens silently without throwing errors.
On the demo apps case:
@State var finishedOnboarding: Bool = UserDefaults.standard.bool(forKey: "onboarding")
The above line is executed before protected data is available thus returning false.
This behaviour led our app to misinterpret nil or defaults as an indication that no data had been stored, causing unexpected logouts, missing payment methods, and, as observed in our demo app, the onboarding screen being presented again.
Key Lessons on Managing App States and Data Protection in iOS
Our experience highlights the importance of considering app states like prewarming and how they interact with iOS's data protection mechanisms. When an app is prewarmed before the device is unlocked, developers need to ensure that their app handles protected data appropriately.
Essential Guidelines for Managing Prewarming and Protected Data in iOS
- Always Check Protected Data Availability: Before accessing UserDefaults, Keychain, or files, verify that UIApplication.shared.isProtectedDataAvailable returns true.
- Defer Sensitive Operations: If protected data is not available, defer operations until you receive UIApplication.protectedDataDidBecomeAvailableNotification.
By understanding how Live Activities and app prewarming interact with iOS's data protection mechanisms, we can prevent unexpected behaviors like user logouts, missing data, or repeated onboarding flows after a device reboot. We hope our findings help other developers navigate this nuanced aspect of iOS development.
Note: For those interested in exploring this issue further, we encourage you to check out our demo app on GitHub. The repository includes instructions on how to reproduce the problem.