Swift
UserDefaults
iOS Development
Programming
CheckIfExists

Check if UserDefault exists - Swift

Master System Design with Codemia

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

Introduction

UserDefaults is the standard key-value store for lightweight persistent settings in iOS and macOS apps. A frequent requirement is checking whether a value exists for a key before using defaults, migrating settings, or deciding whether onboarding has already run. The tricky part is that UserDefaults APIs return typed fallback values for some getters, so checking existence is not always as simple as reading a Boolean or integer.

This article explains reliable existence checks, type-safe wrappers, and migration-safe patterns you can use in production Swift code.

Core Sections

1) Use object(forKey:) for existence checks

object(forKey:) returns nil when the key is missing, making it the most direct way to test presence.

swift
1import Foundation
2
3let defaults = UserDefaults.standard
4let key = "hasSeenOnboarding"
5
6if defaults.object(forKey: key) != nil {
7    print("Key exists")
8} else {
9    print("Key missing")
10}

This avoids ambiguity from typed getters like bool(forKey:), which return false both when key is missing and when value is explicitly false.

2) Avoid false assumptions with typed getters

Consider this common mistake:

swift
if UserDefaults.standard.bool(forKey: "feature_enabled") {
    // Might be false because key is missing, not because user disabled it
}

If key existence matters, check with object(forKey:) first, then parse the typed value.

3) Encapsulate keys in a typed API

A wrapper improves consistency and prevents typo-driven bugs.

swift
1enum DefaultsKey: String {
2    case hasSeenOnboarding
3    case preferredTheme
4}
5
6extension UserDefaults {
7    func exists(_ key: DefaultsKey) -> Bool {
8        object(forKey: key.rawValue) != nil
9    }
10
11    func set(_ value: Bool, for key: DefaultsKey) {
12        set(value, forKey: key.rawValue)
13    }
14}

Typed keys also simplify refactoring and migration scripts.

4) Handle registration defaults correctly

register(defaults:) supplies fallback values but does not create persisted entries. If you check existence after registration, keys may still be absent in persistent storage.

swift
UserDefaults.standard.register(defaults: ["preferredTheme": "system"])
let exists = UserDefaults.standard.object(forKey: "preferredTheme") != nil
// exists may still be false because value is registered, not persisted

Use registration for runtime fallback, not as evidence of stored user choice.

5) Testing and migration strategy

When migrating app versions, distinguish between "key missing" and "old format value." Add tests that start with a clean defaults domain and tests that load legacy values.

swift
let suite = UserDefaults(suiteName: "com.example.tests")!
suite.removePersistentDomain(forName: "com.example.tests")
XCTAssertNil(suite.object(forKey: "some_key"))

Isolated suites prevent accidental coupling with simulator-wide defaults.

6) Production checklist for UserDefaults existence checks

Before shipping this approach in a real project, validate it in a controlled workflow that mirrors production traffic, data shape, and failure modes. Start with one measurable success metric such as latency, error rate, or precision, then define acceptable limits. Run the implementation with representative inputs, not toy samples, and collect logs that explain both successes and failures. If behavior depends on external services or user input, include at least one negative test path so you can confirm how the system reacts when assumptions are violated.

Next, create an operational checklist for rollout. Document required configuration values, version constraints, and environment variables in one place. Add a lightweight smoke test that can run in CI and after deployment. Decide who owns alerts and what threshold should trigger investigation. For high-impact systems, define a rollback switch or feature flag so you can disable the new behavior without a full release cycle.

Finally, capture maintenance notes that future contributors will need: edge cases, known limitations, and links to test fixtures. This short documentation step reduces regressions during refactors and keeps the implementation understandable after the original author rotates to another project.

Common Pitfalls

  • Using bool(forKey:) or integer(forKey:) to infer existence, which confuses missing keys with zero-value defaults.
  • Assuming register(defaults:) persists values and therefore means keys "exist" in storage.
  • Spreading raw string keys through the codebase, leading to silent typos.
  • Migrating key formats without a clear distinction between missing and legacy values.
  • Writing tests against UserDefaults.standard instead of isolated test suites.

Summary

To check whether a UserDefaults key exists, prefer object(forKey:) != nil. Typed getters are useful for reading values, but they are not reliable existence checks. Wrap keys in typed APIs, treat registration defaults separately from persisted state, and test migrations with isolated suites. These practices keep preference handling predictable and reduce subtle configuration bugs across app upgrades.


Course illustration
Course illustration

All Rights Reserved.