Mastering UserDefaults: The Right Way to Use


With UserDefaults, you can store small pieces of data persistently in iOS. Basically, it is a key-value store that saves user preferences, settings, and other lightweight data that needs to persist across app launches.
Using UserDefaults is simple and efficient, which makes it important. Because it provides a straightforward API, it’s a great choice for anyone looking to store small, non-sensitive data quickly.
Avoid using UserDefaults for storing large data sets, sensitive information (like passwords or tokens), or complex objects. For those, consider using the Keychain or Core Data.
Next, you will see the following sections:
Why meaningful names are important for keys?
How to organise the keys across the app?
How to store and retrieve values in a right way?
How to manage modifications at single point?
How to organise UserDefaults using extension?
Use Meaningful Keys
One of the most important aspects of using UserDefaults effectively is creating meaningful and descriptive keys. Using poorly named keys can lead to confusion, collisions, and bugs in your app. Always aim for names that clearly describe the data being stored.
Always use descriptive and consistent keys. This not only improves readability but also reduces the likelihood of key collisions. For example:
// ❌ - Not recommended
let imageData = "imageData"
let loginStatus = "loginStatus"
let accessToken = "accessToken"
let mobileNumber = "mobileNumber"
let launchDate = "launchDate"
// ✅ - Recommended
let userProfileImageData = "userProfileImageData"
let isUserLoggedIn = "isUserLoggedIn"
let userAccessToken = "userAccessToken"
let userPhoneNumber = "userPhoneNumber"
let appFirstLaunchDate = "appFirstLaunchDate"
Why “imageData” is too vague? By specifying the prefix “userProfile,” it’s clear this is the data related to the user’s profile image, avoiding confusion with other potential image data in the app.
Also, the “launchDate” could refer to many things, such as a product launch or session start. The “appFirstLaunchDate” makes it clear that this key stores the date when the app was first launched by the user.
Clear and consistent names make your code more intuitive and efficient to work with. It helps you understand the purpose of key.
Group Related Keys
In large projects, it’s common to have several keys that are related to specific features or parts of your app. Grouping related keys together not only helps with code organisation but also makes your app more scalable and maintainable. Instead of scattering keys throughout your codebase, consider grouping them logically by feature or functionality.
If you have multiple keys related to the same feature, consider grouping them to keep your code organised.
When managing user-related data, grouping keys related to the user profile makes it easier to track user preferences and information in one place. Like this:
struct UserProfileKeys {
static let userProfileImageData = "userProfileImageData"
static let userPhoneNumber = "userPhoneNumber"
static let userAccessToken = "userAccessToken"
static let userDisplayName = "userDisplayName"
static let userEmailAddress = "userEmailAddress"
}
For keys related to app configuration or settings, it makes sense to group them under a dedicated settings struct. Like this:
struct AppSettingsKeys {
static let appFirstLaunchDate = "appFirstLaunchDate"
static let isUserLoggedIn = "isUserLoggedIn"
static let appThemeMode = "appThemeMode"
static let notificationsEnabled = "notificationsEnabled"
}
For onboarding-related features, you might have specific keys to track the user’s progress or completion of onboarding. Like this:
struct OnboardingKeys {
static let hasSeenOnboarding = "hasSeenOnboarding"
static let onboardingStepCompleted = "onboardingStepCompleted"
}
When all related keys are in one place, it’s easier to manage and update them without scattering them throughout the code. As your app grows, grouping helps avoid key collisions and keeps your code structured.
You should make key groups according to feature modules. You can create a key group called “AppSubscriptionKeys” for information related to App Subscriptions. In another example, suppose you have a bookmarking feature in your app and need to store different information in UserDefaults. You can create a key group named “BookmarkKeys”.
Your team members will find it much easier to navigate the code when related keys are logically grouped together.
Store and Retrieve Values
You can take advantages of Swift fundamentals for storing and retrieving the information in UserDefaults. Yes, you can prefer Computed Properties to do that.
Using computed properties (setters and getters) to store and retrieve values from UserDefaults provides a clean and organised way to handle data, improving readability and maintainability of your code.
Here are some example:
// Access and assign a boolean value
var isUserLoggedIn: Bool {
set {
self.set(newValue, forKey: UserDefaults.AppSettingsKeys.isUserLoggedIn)
} get {
return self.bool(forKey: UserDefaults.AppSettingsKeys.isUserLoggedIn)
}
}
// Access and assign a string value
var userPhoneNumber: String? {
set {
self.set(newValue, forKey: UserDefaults.UserProfileKeys.userPhoneNumber)
}
get {
return self.value(forKey: UserDefaults.UserProfileKeys.userPhoneNumber) as? String
}
}
// Access and assign data value
var userProfileImageData: Data? {
set {
self.set(newValue, forKey: UserDefaults.UserProfileKeys.userProfileImageData)
}
get {
return self.value(forKey: UserDefaults.UserProfileKeys.userProfileImageData) as? Data
}
}
There are other ways to store and retrieve the information to UserDefaults, but I personally prefer using the computed properties to do that. Why is this the case? Let me explain.
It encapsulates the logic for storing and retrieving values from UserDefaults in one place. This means you don’t need to call separate methods all over your codebase.
You ensure type safety when retrieving values from UserDefaults. For example, in the
isUserLoggedIn
property, the getter always returns aBool
type, which prevents type-related bugs.If you need to change the key or the method of data storage, you only need to update it in the computed property.
If you need to migrate from UserDefaults to another storage solution, like Keychain, you only need to modify the getter and setter of the properties.
Modify Values
Suppose you are implementing the bookmarking feature for articles, where you need to store the article IDs that the user bookmarks. We’ll create functions to add and remove articles from the bookmark list, all encapsulated within a UserDefaults’s extension. This keeps the code organised and easy to maintain.
How? Let’s walk through the implementation.
The first step is to create a key group that will store information related to bookmark articles. Like this:
// MARK: - BookmarkKeys
struct BookmarkKeys {
static let bookmarkArticleIdentifiers = "bookmarkArticleIdentifiers"
}
// Add more keys if needed for bookmark feature.
Now you can create some functions to add an article to the bookmark list and to remove it from the same list. Like below:
// MARK: - Bookmark Data
extension UserDefaults {
var bookmarkedArticles: [String]? {
set {
self.setValue(newValue, forKey: UserDefaults.BookmarkKeys.bookmarkArticleIdentifiers)
}
get {
self.object(forKey: UserDefaults.BookmarkKeys.bookmarkArticleIdentifiers) as? [String]
}
}
func addBookmarkArticle(articleId: String) {
// create temp array
var identifiers: [String] = []
// store bookmarked articles into temp array if stored any
if let articleArray = bookmarkedArticles {
identifiers = articleArray
}
// append article identifier
// check before append to avoid duplicate entries
// or you can use Set here to avoid duplication
identifiers.append(articleId)
// store updated array of identifier
self.bookmarkedArticles = identifiers
}
func removeBookmarkArticle(articleId: String) {
// create temp array
var identifiers: [String] = []
// write code to remove the article from bookmark list
// store updated array of identifier
self.bookmarkedArticles = identifiers
}
}
Throughout your app, you need to call addBookmarkArticle() function to add an article in the bookmark list. In the same way, call removeBookmarkArticle() function to remove an article from the list.
Why should we do this? From a single point, it’s easy to navigate and modify the storage logic rather than creating functions in other files.
UserDefaults+Extension:
It is likely that you will be using UserDefaults a lot in the real app. To manage the storage from multiple files, it is recommended to create an extension (e.g. UserDefaults+Extension) and keep all logic in this file. The following example shows how to structure your code related to UserDefaults.
extension UserDefaults {
// MARK: - UserProfileKeys
// MARK: - AppSettingsKeys
// MARK: - OnboardingKeys
}
// MARK: - Common Utilities
extension UserDefaults {
// Define common utilities here
}
// MARK: - UserProfile Data
extension UserDefaults {
// Define properties or functions here related to user profile info.
}
// MARK: - AppSettings Data
extension UserDefaults {
// Define properties or functions here related to app settings info.
}
// MARK: - Onboarding Data
extension UserDefaults {
// Define properties or functions here related to onboarding info.
}
In large projects, managing UserDefaults becomes more complex and critical. Without proper management, you can quickly run into issues like key conflicts, data loss, or difficulty debugging. As your app scales, the number of keys in UserDefaults can grow, making it harder to maintain and understand.
Providing a simple way to store and retrieve small amounts of data, UserDefaults are fundamental to iOS development. To make it work effectively, especially in large projects, it is important to follow best practices. By carefully managing your keys, understanding when and where to use UserDefaults, and adhering to good practices, you can ensure your app remains performant and maintainable.
Thank you for taking the time to read this article! If you found it helpful, don’t forget to like, share, and leave a comment with your thoughts or feedback. Your support means a lot!
Keep reading,
— Swiftable
Subscribe to my newsletter
Read articles from Nitin directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Nitin
Nitin
Lead Software Engineer, Technical Writer, Book Author, A Mentor