Every iOS developer uses NSUserDefaults for different purposes; such as persist timestamps and local configuration data. What if NSUserDefaults doesn’t return you the value that you saved previously? The users may see a view second times, which you want them to see it only once. Your analysis events won’t send out as expected because saved timestamp is 0. The worst case is that your core feature is rely on a data that saved inside NSUserDefaults. Returning a wrong value could give users a terrible experience.
The truth is NSUserDefaults could betrayed you. It happens only if your App or App Extension could wake up in the background. I will prove the theory above and show you the NSUserDefaults bug.
Terminology
It’s better defining my terminology here for better understanding the following paragraphs.
First unlock: First ever unlock the device after it’s booted
Code start: The OS system doesn’t have any process of an app. An app started from beginning point.
Attribute
Before we dive into my experiment project. Let’s review what’s the attribute of NSUserDefaults. NSUserDefaults uses NSFileProtectionCompleteUntilFirstUserAuthentication, because it inherits from the App container. In other words, NSUserDefaults is only accessible after FIRST unlock the device. Imagine a messaging app, your phone is dead and you charge it then it’s turned on, before first unlock, you get an remote notification that triggers your extension, your extension code starts. In this case, your extension doesn’t have access for NSUserDefaults, and will get nil value if you try to access a value for a key. We don’t have any surprise here.
Surprise!
How about access it after the first unlock, is NSUserDefaults accessible? The answer is YES and NO. What?!
YES: App cold starts after first unlock, NSUserDefaults is accessible perfectly.
NO: App cold starts before first unlock, NSUserDefaults isn’t accessible before next cold starts, it’s impossible to restart NSUserDefaults to restore to normal state. In the other words, in the same session of before and after first unlock NSUserDefaults isn’t accessible.
Prove
I’m going to prove this bug. I created a project called BHENotificationExtension, this is a simple app has a single view allows input a key value pair data that persist in the NSUserDefaults.
To prove NSUserDefaults isn’t accessible before first unlock, the app need to wake up in the background. So it has an Notification Service Extension that wakes up when remote notification sent to the app.
To look up the data inside of NSUserDefaults, it presents a user notification that shows the data and number of entries. So the app requires notification permission to present a notification.
To push a remote notification I use Pusher. an example of push payload:
Please look the screenshot. “mutable-content” is required for a push payload to wake up the notification extension.
Here is an example of a notification and what the app looks like
Test Cases 1
Proving the NSUserDefaults is accessible in normal state, that the device is after first unlocked everything should work as expected.
- Add a new entry
- Swipe close the app (kill the app)
- Lock the device
- Send a remote push notification
✅ Notification shows the last data and number of entries. NSUserDefaults works as expected.
Test Case 2
Proving the NSUserDefaults isn’t accessible before first unlock, that the device is rebooted and never unlocked.
- Reboot the device
- Send a remote push notification
✅ Notification shows no data found, it is also expected, because NSUserDefaults use NSFileProtectionCompleteUntilFirstUserAuthentication.
Test Case 3
Proving the NSUserDefaults still isn’t accessible even unlock the phone. After I finished the test case 2, I quickly unlock the phone and send more pushes.
- Unlock the phone right after Test Case 2
- Send more pushes
❗️Even I already unlocked the phone, NSUserDefaults still isn’t accessible. Unless this session ended and everything back to normal after next cold start.
What could happen with NSUserDefaults bug
Again, NSUserDefaults bug only happens if your app could wake up at background.
Here is an example: An app supports VOIP push notification. The phone’s batter dead, and another user gives a VOIP call to the app on that phone. The phone won’t get any notification, of course, because phone is off. When the user charges the phone, and the phone is booted, then the push notification wakes up the app and notifies the user there is an missing call. The user will unlock the phone and open the app, he sees so many unexpected views… This case happens if you are saving some flags that controls a view to user.
Work Around
You can trust [UIApplication isProtectedDataAvailable]
method, before a device first unlock it returns NO
, after unlock it returns YES
.
Apple’s documentation:
The value of this property is
NO
if data protection is enabled and the device is currently locked. The value of this property is set toYES
if the device is unlocked or if content protection is not enabled.
If it returns NO, don’t trust any data from NSUserDefaults.
Adding observer for UIApplicationProtectedDataDidBecomeAvailable could notify the app when first unlock happens. Remember you still can’t trust NSUserDefaults if it is the same session as before first unlock.
UNLESS you add a data into NSUserDefaults after isProtectedDataAvailable
, I verified this hacky way on iOS 10 & 11 to reset NSUserDefaults back to normal. Try it your self you will trust me.
Good News
On iOS 12 Apple seems fixed the NSUserDefaults bug. I verified it doesn’t happens on iOS 12 beta 3.
How to use this project
If you like to verify this bug by yourself, you can follow my test cases above by using my experiment project.
You must to have a device to use this project, because it requires push. Download the project at this repository. To make notification works, you need to create a APNS certificate on your developer account. Then create an app group id for sharing NSUserDefaults between the host app and the extension.
Run the app on a device, accept notification permission in order to display notifications, and a device token printed in the console copy it.
In order to trigger an APNS push, I use Pusher. If you have any trouble to make your push works, google it, there are many tutorials for pushing remote notification.
Before doing any test cases, I suggest you make sure you are able to receive an notification and wakes up the notification extension. Also make sure NSUserDefaults is shared by host app and extension.
Thanks
Thank you for reading this post. I hope my experiment could help saving you some time investigating on NSUserDefaults issue for your app.