UserDefaults
July 18, 2026 ยท View on GitHub
Persist graph-aware values by composing UserDefaults synchronization with an
in-memory StateGraph/Stored node.
Overview
StateGraph/Stored is the mutation primitive of StateGraph and always owns its
value in memory. Persistence is modeled as a separate owner rather than as a
replaceable storage backend. StateGraph/GraphUserDefault follows that design:
it synchronizes one UserDefaults key and delegates graph behavior to an internal
Stored<Value>.
This keeps graph dependency tracking independent from Foundation lifecycle and synchronization concerns.
Basic Usage
Declare a default value and a UserDefaults key:
final class Settings {
@GraphUserDefault("theme")
var theme: String = "light"
}
The wrapped property reads and writes UserDefaults. The projected value is the reference-identity synchronization handle:
let settings = Settings()
settings.theme = "dark"
let theme: GraphUserDefault<String> = settings.$theme
theme.wrappedValue = "system"
The projected handle can be retained independently from Settings. It keeps
UserDefaults observation active, and its writes use the same persistence path
as assigning settings.theme.
Local writes publish the corresponding internal Stored change synchronously.
Changes observed through UserDefaults.didChangeNotification also update the
same node, while duplicate notifications for an unchanged value are ignored.
Graph Dependencies
Capture the projected node just as you would capture a projected @GraphStored
property:
final class Settings {
@GraphUserDefault("theme")
var theme: String = "light"
@GraphComputed
var isDarkMode: Bool
init() {
$isDarkMode = .init { [$theme] _ in
$theme.wrappedValue == "dark"
}
}
}
$theme has type GraphUserDefault<String>. Reading its wrappedValue
delegates to the same in-memory Stored primitive used everywhere else in the
graph, so dependency tracking remains unchanged.
Custom Store
Use store: to inject a UserDefaults instance. This is useful for app groups,
tests, and model-level dependency injection:
final class Settings {
@GraphUserDefault var theme: String
init(store: UserDefaults) {
_theme = GraphUserDefault(
wrappedValue: "light",
"theme",
store: store
)
}
}
Named Suites
Use suiteName: when the suite is known at the declaration site:
final class SharedSettings {
@GraphUserDefault("theme", suiteName: "group.com.example.app")
var theme: String = "light"
}
The initializer fails fast if Foundation cannot create the requested suite. It
does not silently fall back to UserDefaults.standard.
Supported Values
StateGraph provides StateGraph/UserDefaultsStorable conformances for these
Foundation property-list values:
BoolIntFloatDoubleStringDataDateURLOptionalwhen its wrapped value conforms toUserDefaultsStorable
Codable values can adopt the protocol and use its JSON-based default implementation:
struct EditorPreferences: Codable, Equatable, Sendable, UserDefaultsStorable {
var lineNumbersEnabled: Bool
}
final class Settings {
@GraphUserDefault("editorPreferences")
var editorPreferences = EditorPreferences(lineNumbersEnabled: true)
}
For a custom representation, implement
UserDefaultsStorable._getValue(from:forKey:defaultValue:) and
UserDefaultsStorable._setValue(to:forKey:) directly.
Ownership
GraphUserDefault is a reference type and its projection returns the same
instance. That identity owns the observer and internal Stored node, and
removes the observer after its final owner releases it. An in-flight
notification keeps the instance alive until its callback finishes; callbacks
never publish graph changes while the UserDefaults access lock or the Stored
node lock is held.