Skip to content
macOSDeep Dive Published Updated 7 min readViews unavailable

UserDefaults on macOS: Preference Keys, Registration, and Migration

Design UserDefaults preferences with typed access, volatile registration defaults, domain-aware troubleshooting, schema migration, and explicit privacy boundaries.

UserDefaults stores app-specific and system-wide settings, and is best suited to nonsensitive preference values that should influence startup behavior. Its apparent simplicity can hide a domain search list, temporary registration values, managed settings, suite behavior, and migration needs. A call such as bool(forKey:) may return a value supplied by a different domain than the app’s persistent preferences, so debugging should begin by understanding where the value came from.

Treat preference names, value types, defaults, and compatibility as a small schema. If a key changes meaning or type, the application needs a migration or compatibility rule. If a value is an entitlement, credential, document, or important user record, UserDefaults is the wrong persistence layer.

Register defaults at startup

The registration domain provides fallback values when a setting is absent from stronger domains. It is volatile and discarded when the app quits, so register your defaults each launch. Registration does not write a user’s chosen preference; it only provides a fallback. A safe preference access layer gives each key one type and one documented default.

import Foundation

enum ReaderPreferences {
    private static let defaults = UserDefaults.standard

    static func registerDefaults() {
        defaults.register(defaults: [
            "showLineNumbers": true,
            "preferredFontSize": 13.0,
            "colorSchemeChoice": "system"
        ])
    }

    static var showLineNumbers: Bool {
        defaults.bool(forKey: "showLineNumbers")
    }

    static var preferredFontSize: Double {
        defaults.double(forKey: "preferredFontSize")
    }
}

Call registerDefaults() early in app startup before views read the settings. The helper deliberately omits setters and validation to keep the example small. Real code should define supported ranges and allowed string values, and should not assume that values loaded from disk have the type or range expected by the newest build.

Use stable key constants to avoid spelling drift. Consider namespacing keys by feature or using a small wrapper that reads and writes each preference in one place. Do not scatter literal strings across view code, command-line tools, extensions, and migration routines. A typo creates a second preference that appears to work but never updates the intended setting.

Understand domains and precedence

When reading a setting, UserDefaults searches domains in order. Apple’s current documentation lists managed, argument, educational managed, app, suite, global, and registration domains, with some domains absent depending on the environment. A command-line or Xcode argument can temporarily override an app preference. Device management can also supply managed settings that the app should not treat as a user choice.

This explains why inspecting the preferences plist alone may not reveal the value returned by the API. Use domain inspection methods for diagnostics and tests, and record the effective value separately from the app’s persisted choice. Do not present a managed or argument override as if the user changed a setting. For a bug report, capture domain names and value types with privacy filtering instead of dumping every preference value.

An app group suite is not an arbitrary way to read another app’s preferences. Apple documents that sandboxed apps can access their own preferences and those of an app extension or app group to which they belong; accessing a suite requires the App Groups entitlement. Adding the bundle identifier of an unrelated app does not grant access to its settings. Validate suite configuration, signing entitlements, and actual process membership rather than assuming a successful initializer implies sharing works.

Store only small, nonsensitive settings

Apple warns not to store personal or sensitive information in defaults because the settings database is stored on disk in an unencrypted form. Do not put passwords, tokens, private keys, full user records, or sensitive browsing history in UserDefaults. Use an appropriate protected store, such as Keychain for credentials, or a database designed for structured durable records.

Preferences should be small, queryable values that are cheap to load. Avoid serializing a large model graph, image, document, or unbounded history into one defaults value. Large preference blobs increase write churn, create difficult migrations, and make corruption harder to isolate. Use files or a database for data with independent lifecycle, integrity, indexing, or backup requirements.

UserDefaults persistence is not an interprocess transaction protocol. App extensions sharing a suite should avoid treating simultaneous read-modify-write sequences as atomic coordination. If multiple processes need consistent shared state, use a storage and locking design appropriate to that requirement. Defaults are convenient for preference propagation, not a general distributed database.

Evolve the preference schema safely

When renaming a key, read the new key first and fall back to the old key once, validate the value, then write the canonical new key. Remove the old key after a successful migration if it will not be needed for downgrade support. If changing a type, define how to map legacy values and what happens when the old value is malformed. Never silently treat an invalid value as a meaningful zero or false unless that is the documented product default.

For string-backed enums, keep a stable raw representation and provide an unknown-value fallback. A future build may add a new choice while an older extension or app version is still running. Validate values at the boundary and choose a safe behavior for unknown cases. For numeric settings, clamp to an explicit supported range only when that is the intended compatibility rule; otherwise report or reset invalid input with a diagnostic.

Make migration idempotent. A crash between reading an old key and writing the new one should not corrupt the next launch. Keep the migration small and deterministic, then test from real preference fixtures created by supported historical versions. If the app has a major setting model change, version the preferences collectively or store an explicit schema marker so migrations can be reasoned about.

Test and debug without confusing the domains

Tests should isolate the defaults suite so one test cannot pollute another. Use a uniquely named suite for disposable tests, register the same fallback values as production, and remove the test domain during teardown. Do not call deprecated global reset methods as a shortcut. Verify both effective reads and the persistent values actually written to the intended domain.

Launch-time argument overrides are useful for test scenarios such as enabling a diagnostic mode or changing a rendering option. They are volatile and have higher precedence than the app domain. Ensure release builds do not accidentally ship test launch arguments or treat argument-domain values as durable user preferences.

The synchronize() method is deprecated and unnecessary; Apple’s documentation says it waits for pending asynchronous updates and should not be used. Do not add it after every write in an attempt to force immediate persistence. Design the app so a preference write is not the only record of a high-value operation, and use a storage layer with the required durability guarantees for that operation.

Change notifications and UI consistency

If multiple views read one preference, centralize observation so a change updates all dependent UI. Keep presentation state distinct from the persisted choice: a settings window may edit a temporary draft and commit only after Apply, or save immediately and support Undo. Choose one model and communicate it clearly. Avoid having each view maintain a separate in-memory copy that can diverge from the effective defaults value.

When an app group value is changed by an extension, define how the main app learns about it and refreshes relevant state. Do not poll aggressively or assume every process receives an immediate notification. The consumers should re-read the setting when they resume or handle a relevant lifecycle event, with a last-known update token if the app needs to detect change.

Acceptance matrix

Test first launch with no app domain, a user override, an argument override, a managed value, a registered fallback, an app-group suite with valid entitlement, a suite without entitlement, malformed historical values, unknown enum strings, key migration interruption, two processes writing concurrently, and a test suite that must not leak state. Assert exact effective values and the domain that supplied them.

Instrument migration version, key identifier, value type, and validation outcome without recording sensitive values. If a preference unexpectedly differs from the UI, collect the ordered domain sources rather than clearing the entire defaults database. Deleting a user’s settings can erase their intended behavior and disguise a precedence or key-name bug.

UserDefaults works well as a preference system when keys have stable meaning, fallback values are registered, and domain precedence is understood. It is not secure storage, a transactional database, or a place for large records. Make those boundaries explicit and migrations remain manageable as the app evolves.

Related:

Sources:

Comments