User Defaults are the go-to solution for Swift applications to store preferences that persist across launches of your app. It’s a key-value store backed by a property list (plist) file. Due to this type of backing store, you need to be aware of the supported storage types.
There are a few best practices when working with User Defaults. I can also recommend specific solutions based on implementation experience from using it in tens of apps. Let’s dive in!
What are User Defaults?
Apps commonly use User Defaults to store users’ preferences. You can store preferences like the user’s favorite stocks or save specific user states like “user has seen the onboarding.”
The code to store preferences like these could look as follows:
UserDefaults.standard.set(true, forKey: "has-seen-onboarding")
UserDefaults.standard.set(["AAPL", "TSLA"], forKey: "favorite-stocks")
print(UserDefaults.standard.bool(forKey: "has-seen-onboarding"))
// Prints: true
print(UserDefaults.standard.array(forKey: "favorite-stocks"))
// Prints: ["AAPL", "TSLA"]
In this case, we’re using the standard
user defaults container. In most cases, this will be sufficient. However, you might want to consider using group user defaults.
Sharing User Defaults with other apps and extensions
Using so-called app groups, you can share the User Defaults container with other apps and extensions. I highly recommend using this technique for any app from the start. Even though there might not be a need to share preferences right now, you’ll thank yourself later if you add extensions that need to read or write preferences from the main app.
To configure App Groups you need to add a new capability to your project’s settings:
You can find detailed instructions inside Apple’s documentation. Once configured, you can create a new instance using the group identifier:
extension UserDefaults {
static let group = UserDefaults(suiteName: "group.your.identifier")
}
You can now access the shared group container anywhere by making use of the static property:
UserDefaults.group.set(["AAPL", "TSLA"], forKey: "favorite-stocks")
Any app or extension configured with the same app group will now be able to read and write the favorite stocks. I’m using this technique inside Stock Analyzer to populate widgets based on favorite stocks configured in the main application.
The types of data User Defaults support
Property lists must support the objects you store inside User Defaults. You’ll run into the following error as soon as you write an unsupported object:
*** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘Attempt to insert non-property list object UserDefaults.Stock(symbol: “AAPL”) for key last-opened-stock’
In this case, I tried to store an encodable object:
struct Stock: Decodable {
let symbol: String
}
UserDefaults.group.set(Stock(symbol: "AAPL"), forKey: "last-opened-stock")
Anytime you run into an exception like this, you must convert the data before storing it. You can use a JSONEncoder
to encode the instance to data and decode it when you read the value.
User Defaults support the following types:
- Data
- Strings
- Numbers (NSNumber)
- Dates
- Arrays
- Dictionaries
- Booleans
If your type is not on this list, you need to find a way to convert it to any of the supported types.
Responding to changes
While you can use the didChangeNotification
to observe for changes, I recommend looking into managed solutions like this User Defaults Property Wrapper.
Monitoring UserDefaults changes
While working on features that interact with User Defaults, you want to have a way to monitor changes in real-time. To solve this problem, I built a User Defaults Editor into RocketSim, allowing you to edit and monitor key-value pairs in real time.
For example, I’m working on a tooltip in the following video that shows inside the WeTransfer application. The tooltip should only be showing once per user, and I want to ensure the User Defaults key hasShownUploadFilesTooltip
updates accordingly. You can open the editor by clicking the Perform button and selecting the User Defaults plist file.
The editor monitors values constantly and flashes a blue background color when the value changes. At the same time, I can reset the value using the switch and restart the application using RocketSim to see if the tooltip shows again.
You can imagine this drastically speeds up the workflow of testing implementations that rely on UserDefaults. What’s best is that you can get started for free and test the editor with any standard suites by installing RocketSim from the Mac App Store.
Overriding UserDefaults for debugging purposes
While using RocketSim helps you to change and debug optimally, you might want to use scheme settings to override User Defaults during debugging. For that, I wrote a dedicated post: Overriding UserDefaults for improved productivity.
Alternatives considered
User Defaults are a great solution in most cases, but you might want to explore other solutions in case you’re storing sensitive data or when you want to access data across devices.
Keychain for security
User Defaults are not secure enough to store sensitive data. User credentials, API keys, or other sensitive data should be stored inside the keychain instead.
CloudKit for cross platform
Consider using the NSUbiquitousKeyValueStore
in case you want the preferences to be accessible from other Apple devices with your app installed. It’s a similar key-value store but uses iCloud as a backing store.
Conclusion
You can store preferences using User Defaults and capture state across app launches. App Groups are great for sharing preferences with other apps and extensions, and you need to keep an eye on the types of data you can store. By monitoring the backing store, you’ll ensure no unexpected stored data. When you need to access data across devices or when you want to store sensitive data, it’s better to look at alternatives.
If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Thanks!