--- source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/user-defaults-storage.mdc confidence: 0.9 namespace: work last_synced: 2026-04-28 alwaysApply: false --- # User Defaults Settings Storage and Reading ## ✅ RECOMMENDED - KVO Pattern with KeyValueStore Use the KVO pattern with KeyValueStore for all new persistent settings: ```swift // ✅ CORRECT - KVO pattern with KeyValueStore struct AppearancePreferencesUserDefaultsPersistor: AppearancePreferencesPersistor { enum Key: String { case newTabPageIsOmnibarVisible = "new-tab-page.omnibar.is-visible" case newTabPageIsProtectionsReportVisible = "new-tab-page.protections-report.is-visible" case userPreferences = "user.preferences" case lastUpdateCheck = "last.update.check" } private let keyValueStore: KeyValueStoring init(keyValueStore: KeyValueStoring) { self.keyValueStore = keyValueStore } var isOmnibarVisible: Bool { get { (try? keyValueStore.object(forKey: Key.newTabPageIsOmnibarVisible.rawValue) as? Bool) ?? true } set { try? keyValueStore.set(newValue, forKey: Key.newTabPageIsOmnibarVisible.rawValue) } } var isProtectionsReportVisible: Bool { get { (try? keyValueStore.object(forKey: Key.newTabPageIsProtectionsReportVisible.rawValue) as? Bool) ?? false } set { try? keyValueStore.set(newValue, forKey: Key.newTabPageIsProtectionsReportVisible.rawValue) } } var userPreferences: [String: String] { get { (try? keyValueStore.object(forKey: Key.userPreferences.rawValue) as? [String: String]) ?? [:] } set { try? keyValueStore.set(newValue, forKey: Key.userPreferences.rawValue) } } var lastUpdateCheck: Date { get { (try? keyValueStore.object(forKey: Key.lastUpdateCheck.rawValue) as? Date) ?? Date.distantPast } set { try? keyValueStore.set(newValue, forKey: Key.lastUpdateCheck.rawValue) } } } ``` ## Key Guidelines for KVO Pattern 1. **Use struct conforming to protocol** - Follow the persistor pattern 2. **Define keys as enum with String raw values** - Use kebab-case for key names 3. **Use KeyValueStoring protocol** - Not direct UserDefaults access 4. **Computed properties with get/set** - Handle storage operations in accessors 5. **Use try? for error handling** - KeyValueStore operations can throw 6. **Provide default values** - Use nil coalescing operator (??) for defaults 7. **Inject KeyValueStore in init** - Enable dependency injection and testing ## Advanced Pattern for Optional Values ```swift // ✅ CORRECT - Optional values pattern struct SettingsUserDefaultsPersistor: SettingsPersistor { enum Key: String { case optionalUserName = "user.name" case optionalTheme = "app.theme" } private let keyValueStore: KeyValueStoring init(keyValueStore: KeyValueStoring) { self.keyValueStore = keyValueStore } var optionalUserName: String? { get { try? keyValueStore.object(forKey: Key.optionalUserName.rawValue) as? String } set { if let value = newValue { try? keyValueStore.set(value, forKey: Key.optionalUserName.rawValue) } else { try? keyValueStore.removeObject(forKey: Key.optionalUserName.rawValue) } } } var selectedTheme: Theme? { get { guard let rawValue = try? keyValueStore.object(forKey: Key.optionalTheme.rawValue) as? String else { return nil } return Theme(rawValue: rawValue) } set { if let value = newValue { try? keyValueStore.set(value.rawValue, forKey: Key.optionalTheme.rawValue) } else { try? keyValueStore.removeObject(forKey: Key.optionalTheme.rawValue) } } } } ``` ## Platform-Specific Storage ```swift // ✅ CORRECT - Platform-specific KeyValueStore usage struct PlatformSettingsUserDefaultsPersistor: PlatformSettingsPersistor { enum Key: String { case platformSpecificSetting = "platform.specific.setting" } private let keyValueStore: KeyValueStoring init(keyValueStore: KeyValueStoring) { self.keyValueStore = keyValueStore } var platformSpecificSetting: Bool { get { #if os(iOS) return (try? keyValueStore.object(forKey: Key.platformSpecificSetting.rawValue) as? Bool) ?? false #elseif os(macOS) return (try? keyValueStore.object(forKey: Key.platformSpecificSetting.rawValue) as? Bool) ?? true #endif } set { try? keyValueStore.set(newValue, forKey: Key.platformSpecificSetting.rawValue) } } } ``` ## 🚫 DEPRECATED - @UserDefaultsWrapper Pattern The following pattern is deprecated and should not be used for new code: ```swift // ❌ DEPRECATED - Do not use @UserDefaultsWrapper for new code extension AppUserDefaults { @UserDefaultsWrapper(key: .newFeatureEnabled, defaultValue: false) var newFeatureEnabled: Bool @UserDefaultsWrapper(key: .lastUpdateCheck, defaultValue: Date.distantPast) var lastUpdateCheck: Date } ``` ## Migration from Property Wrappers When migrating from `@UserDefaultsWrapper` to the KVO pattern: 1. **Create a new persistor struct** - Following the naming convention `*UserDefaultsPersistor` 2. **Define keys enum** - Convert string keys to enum cases 3. **Convert properties** - Transform @UserDefaultsWrapper properties to computed properties 4. **Update injection** - Pass KeyValueStore through dependency injection 5. **Preserve key names** - Ensure existing UserDefaults keys remain unchanged ## Testing Pattern ```swift // ✅ CORRECT - Testing with mock KeyValueStore class MockKeyValueStore: KeyValueStoring { private var storage: [String: Any] = [:] func object(forKey key: String) throws -> Any? { return storage[key] } func set(_ value: Any, forKey key: String) throws { storage[key] = value } func removeObject(forKey key: String) throws { storage.removeValue(forKey: key) } } // In tests let mockStore = MockKeyValueStore() let persistor = AppearancePreferencesUserDefaultsPersistor(keyValueStore: mockStore) persistor.isOmnibarVisible = true XCTAssertTrue(persistor.isOmnibarVisible) ``` ## What NOT to Do ```swift // ❌ INCORRECT - Direct UserDefaults access var newFeatureEnabled: Bool { get { return UserDefaults.standard.bool(forKey: "newFeature") } set { UserDefaults.standard.set(newValue, forKey: "newFeature") } } // ❌ INCORRECT - Using @UserDefaultsWrapper for new code @UserDefaultsWrapper(key: .newFeatureEnabled, defaultValue: false) var newFeatureEnabled: Bool // ❌ INCORRECT - Not handling errors var setting: Bool { get { keyValueStore.object(forKey: "key") as? Bool ?? false } // Missing try? set { keyValueStore.set(newValue, forKey: "key") } // Missing try? } // ❌ INCORRECT - Not using enum for keys var setting: Bool { get { (try? keyValueStore.object(forKey: "hardcoded-key") as? Bool) ?? false } set { try? keyValueStore.set(newValue, forKey: "hardcoded-key") } } ``` The KVO pattern with KeyValueStore provides better testability, error handling, and dependency injection while maintaining type safety and consistency across the codebase.