English | 中文
After importing ObservableDefaults, you can annotate your class with @ObservableDefaults to automatically manage UserDefaults synchronization:
import ObservableDefaults
@ObservableDefaults
class Settings {
var name: String = "Fatbobman"
var age: Int = 20
var nickname: String? = nil // Optional support
}observableDefaults-1_2024-10-09_14.54.53-1.mp4
This macro automatically:
- Associates the
nameandageproperties withUserDefaultskeys. - Listens for external changes to these keys and updates the properties accordingly.
- Notifies SwiftUI views of changes precisely, avoiding unnecessary redraws.
For cloud-synchronized data that automatically syncs across devices, use the @ObservableCloud macro:
import ObservableDefaults
@ObservableCloud
class CloudSettings {
var number = 1
var color: Colors = .red
var style: FontStyle = .style1
var cloudName: String? = nil // Optional support
}observableCloud-demo1.mp4
This macro automatically:
- Associates properties with
NSUbiquitousKeyValueStorefor iCloud synchronization - Listens for external changes from other devices and updates properties accordingly
- Provides the same precise SwiftUI observation as
@ObservableDefaults - Supports development mode for testing without CloudKit container setup
Both @ObservableDefaults and @ObservableCloud classes work identically in SwiftUI views:
import SwiftUI
struct ContentView: View {
@State var settings = Settings() // UserDefaults-backed
@State var cloudSettings = CloudSettings() // iCloud-backed
var body: some View {
VStack {
// Local settings
Text("Name: \(settings.name)")
TextField("Enter name", text: $settings.name)
// Cloud-synchronized settings
Text("Username: \(cloudSettings.username)")
TextField("Enter username", text: $cloudSettings.username)
}
.padding()
}
}The library provides additional macros for finer control:
@ObservableOnly: The property is observable but not stored inUserDefaults.@Ignore: The property is neither observable nor stored inUserDefaults.@DefaultsKey: Specifies a customUserDefaultskey for the property.@DefaultsBacked: The property is stored inUserDefaultsand observable.@DefaultsBackeddoes not supportwillSet/didSet.
@ObservableDefaults
public class LocalSettings {
@DefaultsKey(userDefaultsKey: "firstName")
public var name: String = "fat"
public var age = 109 // Automatically backed by UserDefaults
@ObservableOnly
public var height = 190 // Observable only, not persisted
@Ignore
public var weight = 10 // Neither observable nor persisted
}Similar macro support with cloud-specific options:
@ObservableOnly: The property is observable but not stored inNSUbiquitousKeyValueStore.@Ignore: The property is neither observable nor stored.@CloudKey: Specifies a customNSUbiquitousKeyValueStorekey for the property.@CloudBacked: The property is stored inNSUbiquitousKeyValueStoreand observable.@CloudBackeddoes not supportwillSet/didSet.
@ObservableCloud
public class CloudSettings {
@CloudKey(keyValueStoreKey: "user_display_name")
public var username: String = "Fatbobman"
public var theme: String = "light" // Automatically cloud-backed
@ObservableOnly
public var localCache: String = "" // Observable only, not synced to cloud
@Ignore
public var temporaryData: String = "" // Neither observable nor persisted
}With autoInit: true, the macro generates this initializer for the Settings class used above:
public init(
userDefaults: Foundation.UserDefaults? = nil,
ignoreExternalChanges: Bool? = nil,
prefix: String? = nil,
ignoredKeyPathsForExternalUpdates: [PartialKeyPath<Settings>] = []
)Parameters:
userDefaults: A per-instance store override.nilkeeps the store selected by the macro'ssuiteName, or.standardwhen no suite is configured.ignoreExternalChanges: A per-instance override.nilpreserves the enclosing macro'signoreExternalChangesvalue.prefix: A per-instance key-prefix override.nilpreserves the enclosing macro'sprefixvalue.ignoredKeyPathsForExternalUpdates: Properties excluded from external storage update handling for this instance (default: none).
With the default @ObservableCloud configuration, the macro generates:
public init(
prefix: String? = nil,
syncImmediately: Bool = false,
developmentMode: Bool = false
)Parameters:
prefix: A per-instance key-prefix override.nilpreserves the enclosing macro'sprefixvalue.syncImmediately: Controls whether each write forces immediate synchronization.developmentMode: Selects memory-backed development storage instead of iCloud storage.
The generated default literals for syncImmediately and developmentMode match the values supplied to the enclosing macro. For example, @ObservableCloud(syncImmediately: true) generates syncImmediately: Bool = true; an explicit initializer argument still overrides that generated default.
// UserDefaults-backed settings
@State var settings = Settings(
userDefaults: .standard,
ignoreExternalChanges: false,
prefix: "myApp_"
)
// Cloud-backed settings
@State var cloudSettings = CloudSettings(
prefix: "myApp_",
syncImmediately: true,
developmentMode: false
)You can set parameters directly in the @ObservableDefaults macro:
suiteName: TheUserDefaultssuite name (default is empty, which uses.standard).ignoreExternalChanges: Whether to ignore external changes.prefix: A prefix forUserDefaultskeys.autoInit: Whether to automatically generate the initializer (default istrue).observeFirst: Observation priority mode (default isfalse).limitToInstance: Whether to limit observations to the specific UserDefaults instance (default istrue). Set tofalsefor App Group cross-process synchronization.defaultIsolationIsMainActor: Whether the target uses MainActor as its default isolation (default isfalse).
@ObservableDefaults(autoInit: false, ignoreExternalChanges: true, prefix: "myApp_")
class Settings {
@DefaultsKey(userDefaultsKey: "fullName")
var name: String = "Fatbobman"
}
// For App Group cross-process synchronization
@ObservableDefaults(
suiteName: "group.myapp",
prefix: "myapp_",
limitToInstance: false
)
class SharedSettings {
var lastUpdate: Date = Date()
}The cloud macro provides similar configuration options:
autoInit: Whether to automatically generate the initializer (default istrue).prefix: A prefix forNSUbiquitousKeyValueStorekeys.observeFirst: Observation priority mode (default isfalse).syncImmediately: Whether to force immediate synchronization (default isfalse).developmentMode: Whether to use memory storage for testing (default isfalse).defaultIsolationIsMainActor: Whether the target uses MainActor as its default isolation (default isfalse).
@ObservableCloud(
autoInit: true,
prefix: "myApp_",
observeFirst: false,
syncImmediately: true,
developmentMode: false
)
class CloudSettings {
@CloudKey(keyValueStoreKey: "user_theme")
var theme: String = "light"
}The @ObservableCloud macro supports development mode for testing without CloudKit setup:
@ObservableCloud(developmentMode: true)
class CloudSettings {
var setting1: String = "value1" // Uses memory storage
var setting2: Int = 42 // Uses memory storage
}Development mode is automatically enabled when:
- Explicitly set via
developmentMode: true - Running in SwiftUI Previews (
XCODE_RUNNING_FOR_PREVIEWSenvironment variable) OBSERVABLE_DEFAULTS_DEV_MODEenvironment variable is set to "true"
If you set autoInit to false for either macro, you need to create your own initializer:
// For @ObservableDefaults
init() {
observerStarter() // Start listening for UserDefaults changes
}
// For @ObservableCloud
init() {
// Start Cloud Observation only in production mode
if !_developmentMode_ {
_cloudObserver = CloudObservation(host: self, prefix: _prefix)
}
}Both macros support "Observe First" mode, where properties are observable by default but only explicitly marked properties are persisted:
@ObservableDefaults(observeFirst: true)
public class LocalSettings {
public var name: String = "fat" // Observable only
public var age = 109 // Observable only
@DefaultsBacked(userDefaultsKey: "myHeight")
public var height = 190 // Observable and persisted to UserDefaults
@Ignore
public var weight = 10 // Neither observable nor persisted
}@ObservableCloud(observeFirst: true)
public class CloudSettings {
public var localSetting: String = "local" // Observable only
public var tempData = "temp" // Observable only
@CloudBacked(keyValueStoreKey: "user_theme")
public var theme: String = "light" // Observable and synced to iCloud
@Ignore
public var cache = "cache" // Neither observable nor persisted
}@DefaultsBackedand@CloudBackeddo not supportwillSet/didSet.@ObservableOnlysupportswillSet/didSet.- In Observe First mode, properties automatically marked as
@ObservableOnlyalso supportwillSet/didSet.
Both macros fully support Optional properties:
@ObservableDefaults
class SettingsWithOptionals {
var username: String? = nil
var age: Int? = 25
var isEnabled: Bool? = true
@DefaultsKey(userDefaultsKey: "custom-optional-key")
var customOptional: String? = nil
}
@ObservableCloud
class CloudSettingsWithOptionals {
var cloudUsername: String? = nil
var preferences: [String]? = nil
@CloudKey(keyValueStoreKey: "user-settings")
var userSettings: [String: String]? = nil
}Both macros support properties conforming to Codable for complex data persistence:
@ObservableDefaults
class LocalStore {
var people: People = .init(name: "fat", age: 10)
}
struct People: Codable {
var name: String
var age: Int
}@ObservableCloud
class CloudStore {
var userProfile: UserProfile = .init(name: "fat", preferences: .init())
}
struct UserProfile: Codable {
var name: String
var preferences: UserPreferences
}
struct UserPreferences: Codable {
var theme: String = "light"
var fontSize: Int = 14
}Enums whose RawValue already conforms to the property-list set (for example String, Int, etc.) are persisted automatically via their raw value:
enum Theme: String {
case light
case dark
case system
}
@ObservableDefaults
class AppearanceSettings {
var theme: Theme = Theme.system
}When a type conforms to both RawRepresentable and Codable, the library will prioritize the RawRepresentable storage method, storing values using their raw representation rather than JSON encoding. This ensures backward compatibility with existing data and provides more efficient storage for enum types.
These rules apply to both @ObservableDefaults (UserDefaults) and @ObservableCloud (NSUbiquitousKeyValueStore).
When a type matches multiple constraints, the implementation chooses the most specific path in this order:
RawRepresentable & PropertyListValue & CodableRawRepresentable & PropertyListValueRawRepresentable(whereRawValueis a PropertyList-compatible type)PropertyListValue & CodablePropertyListValueCodableonly (JSONDatapath; intentionally lower priority)
RawRepresentable-based paths: persistrawValue.- Example:
String/Intraw values are stored directly asString/Int.
- Example:
PropertyListValuepaths: persist the value directly as PropertyList-compatible objects.Codable-only path: persist JSON-encodedData.URL/NSURLpaths: persist JSON-encodedDatausingURL's Codable representation. They are not passed directly toUserDefaultsorNSUbiquitousKeyValueStoreas property-list objects.- Optional values:
- non-
nil: stored using the same rules above nil: key is removed
- non-
For RawRepresentable & PropertyListValue (including RawRepresentable & PropertyListValue & Codable):
- Read attempts
rawValueformat first. - If that fails, read falls back to direct
PropertyListValuecasting.
This fallback keeps older data readable when a property was previously persisted via direct PropertyList format and later evolved to a RawRepresentable type.
If you also read/write these keys directly outside the macros, use the same format rules to avoid mismatches.
- Use
rawValuefor allRawRepresentable-based properties. - Use direct PropertyList values for PropertyList paths.
- Use JSON
Dataonly forCodable-only properties. - Use JSON
Dataencoded fromURLforURL/NSURLproperties. - Key naming follows macro key resolution:
- default:
prefix + propertyName - custom key:
@DefaultsKey/@CloudKey
- default:
Example (UserDefaults):
// For RawRepresentable-backed property (rawValue: String)
defaults.set(theme.rawValue, forKey: "app_theme")
// For Codable-only property
defaults.set(try JSONEncoder().encode(profile), forKey: "app_profile")
// For URL / NSURL properties
defaults.set(try JSONEncoder().encode(homepageURL), forKey: "app_homepage")It's recommended to manage storage data separately from your main application state:
@Observable
class ViewState {
var selection = 10
var isLogin = false
let localSettings = LocalSettings() // UserDefaults-backed
let cloudSettings = CloudSettings() // iCloud-backed
}
struct ContentView: View {
@State var state = ViewState()
var body: some View {
VStack(spacing: 30) {
// Local settings
Text("Local Name: \(state.localSettings.name)")
Button("Modify Local Setting") {
state.localSettings.name = "User \(Int.random(in: 0...1000))"
}
// Cloud settings
Text("Cloud Username: \(state.cloudSettings.username)")
Button("Modify Cloud Setting") {
state.cloudSettings.username = "CloudUser \(Int.random(in: 0...1000))"
}
}
.buttonStyle(.bordered)
}
}