NSUserDefaults
iOS development
Swift programming
key-value storage
app settings

NSUserDefaults - How to tell if a key exists

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In UserDefaults, checking whether a key exists is not the same as reading the stored value. Several typed getters return fallback values when the key is missing, which means a missing key and an intentionally stored default-like value can look identical unless you test for existence explicitly.

Why Typed Getters Are Not Enough

Methods such as bool(forKey:) and integer(forKey:) are convenient, but they do not tell you whether a key was ever written.

swift
1import Foundation
2
3let defaults = UserDefaults.standard
4
5print(defaults.bool(forKey: "hasSeenOnboarding"))
6print(defaults.integer(forKey: "launchCount"))

If those keys do not exist, the results are still false and 0. That behavior is helpful for simple reads but misleading when your logic depends on distinguishing missing data from stored data.

The Reliable Existence Check

To check whether a key exists, use object(forKey:) and compare the result to nil.

swift
1import Foundation
2
3let defaults = UserDefaults.standard
4let key = "hasSeenOnboarding"
5
6let exists = defaults.object(forKey: key) != nil
7print(exists)

This works because object(forKey:) only returns nil when the key is absent. If the key exists, you get an object representation of the stored value.

Wrap It in a Small Helper

A helper method keeps call sites readable and reduces copy-pasted logic.

swift
1import Foundation
2
3extension UserDefaults {
4    func contains(_ key: String) -> Bool {
5        object(forKey: key) != nil
6    }
7}
8
9let defaults = UserDefaults.standard
10print(defaults.contains("hasSeenOnboarding"))

This is usually the best general-purpose answer for Swift codebases that interact with UserDefaults regularly.

Read the Typed Value After Checking Existence

When the distinction matters, check first and then read the typed value.

swift
1import Foundation
2
3let defaults = UserDefaults.standard
4let key = "launchCount"
5
6if defaults.object(forKey: key) != nil {
7    let count = defaults.integer(forKey: key)
8    print("Stored launch count: \(count)")
9} else {
10    print("No launch count stored yet")
11}

This is especially useful during onboarding flows, migrations, or feature-rollout logic where the first run must be handled differently from a real stored setting.

Keep Keys Centralized

Raw string keys are easy to mistype. A small enum or wrapper object makes the storage contract much safer.

swift
1import Foundation
2
3enum DefaultsKey: String {
4    case hasSeenOnboarding
5    case launchCount
6    case preferredTheme
7}
8
9final class SettingsStore {
10    private let defaults: UserDefaults
11
12    init(defaults: UserDefaults = .standard) {
13        self.defaults = defaults
14    }
15
16    func exists(_ key: DefaultsKey) -> Bool {
17        defaults.object(forKey: key.rawValue) != nil
18    }
19
20    func setLaunchCount(_ count: Int) {
21        defaults.set(count, forKey: DefaultsKey.launchCount.rawValue)
22    }
23}

Centralizing keys reduces accidental mismatches across different parts of the app.

dictionaryRepresentation() Is Usually Too Broad

You can also inspect all stored keys with dictionaryRepresentation(), but that is usually a debugging tool rather than the best existence check for production code.

swift
1import Foundation
2
3let defaults = UserDefaults.standard
4let exists = defaults.dictionaryRepresentation().keys.contains("preferredTheme")
5print(exists)

This works, but it does more work than asking for one key directly. Use it when you want a broad inspection of stored values, not when you only need a single existence test.

Common Pitfalls

A common mistake is treating bool(forKey:) == false as proof that the key is missing. It may just mean the stored value is false.

Another mistake is using string(forKey:) as a universal existence check. That only works reliably if the stored type is actually a string.

Developers also forget that removeObject(forKey:) truly deletes the key. After removal, typed getters fall back to their default-return behavior again.

Summary

  • Use object(forKey:) != nil to test whether a UserDefaults key exists.
  • Typed getters such as bool(forKey:) and integer(forKey:) do not prove key existence.
  • Check existence first when missing and stored default-like values must be distinguished.
  • Wrap keys in an enum or helper to reduce string-typing mistakes.
  • Treat dictionaryRepresentation() as a debugging aid, not the default existence check.

Course illustration
Course illustration

All Rights Reserved.