What does UserDefaults return for a key that is not set, how do registered defaults work, and what should and should not be stored there?
Predict first
What do missing, registered and removed keys return?
import Foundation
let suite = "test.\(UUID().uuidString)"
let d = UserDefaults(suiteName: suite)!
d.register(defaults: ["volume": 7, "name": "guest"])
print("registered:", d.integer(forKey: "volume"), d.string(forKey: "name") ?? "nil")
print("missing int:", d.integer(forKey: "nope"), "bool:", d.bool(forKey: "nope"), "object:", d.object(forKey: "nope") as Any)
d.set(0, forKey: "count")
print("stored zero is distinguishable:", d.object(forKey: "count") != nil, d.integer(forKey: "count"))
d.removeObject(forKey: "volume")
print("after remove, back to registered:", d.integer(forKey: "volume"))
d.set("12", forKey: "age")
print("string read as integer:", d.integer(forKey: "age"))
struct P: Codable { var a: Int }
d.set(try! JSONEncoder().encode(P(a: 3)), forKey: "p")
print("Codable stored as Data:", try! JSONDecoder().decode(P.self, from: d.data(forKey: "p")!).a)
d.removePersistentDomain(forName: suite)
- registered: 7 guest / missing int: 0 bool: false object: nil / stored zero is distinguishable: true 0 / after remove, back to registered: 7 / string read as integer: 12 / Codable stored as Data: 3
- missing int: nil
- registered: 0 guest
Mental model. UserDefaults is a pinboard with sticky notes. Asking for a note that is not there gives you a blank note, which looks the same as a note that says zero.
The typed getters never fail: integer(forKey:) returns 0, bool(forKey:) returns false and double(forKey:) returns 0.0 for a missing key, so you cannot tell “unset” from “set to zero” with them. Use object(forKey:) (which is nil when missing) when the difference matters; in the check a stored 0 is found while an unset key is not. register(defaults:) supplies fallback values that are never written to disk: the registered 7 is returned until you set a value, and after removeObject the key falls back to the registered value again. Call it once at launch, before the first read, because registrations are per process (not persisted).
Values must be property-list types (String, numbers, Bool, Date, Data, arrays and dictionaries of those, URL through its own setter). Reads are forgiving in a way that hides bugs: a string "12" is returned by integer(forKey:) as 12. For a small Codable value encode it to Data (or use @AppStorage for simple SwiftUI values). Keep UserDefaults for small preferences and flags, not for large data, secrets (use the Keychain, Q329), or anything that must be transactional: it is a property list loaded into memory, synchronised lazily, and an unbounded value slows launch. Use a suite (UserDefaults(suiteName:), with an App Group id) to share preferences with an extension, and a unique suite plus removePersistentDomain in tests. Wrap keys in a typed enum or property wrapper so the key strings and defaults live in one place.
Typed getters return zero values for missing keys; registered defaults are the fallback and are not persisted.
Common traps
- Using
bool(forKey:)to decide whether a setting was ever chosen. A missing key and an explicitfalselook the same; checkobject(forKey:). - Storing tokens or large blobs. It is unencrypted, in backups, and read at launch.
They might follow up with
- Do I need to call
synchronize()? No, it is unnecessary in modern code; the system persists changes. - How do I observe a change? KVO on the key path,
UserDefaults.didChangeNotification, or@AppStoragein SwiftUI.
Related questions
- 13.8.2 What are the FileManager basics for saving a document: creating directories, replacing a file, and the…
- 13.11.1 Which OSStatus values matter when you use SecItem functions, and how do you write a correct save (upsert) of…
- 13.1.1 UserDefaults, Keychain, files, SQLite, Core Data or SwiftData: how do you choose, and what survives deletion…
- 13.1.2 How does the Keychain work, how do you add, read and update an item, and what do accessibility classes and…
What are the FileManager basics for saving a document: creating… →