7.2 KiB
7.2 KiB
source, confidence, namespace, last_synced, alwaysApply
| source | confidence | namespace | last_synced | alwaysApply |
|---|---|---|---|---|
| ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/user-defaults-storage.mdc | 0.9 | work | 2026-04-28 | false |
User Defaults Settings Storage and Reading
✅ RECOMMENDED - KVO Pattern with KeyValueStore
Use the KVO pattern with KeyValueStore for all new persistent settings:
// ✅ 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
- Use struct conforming to protocol - Follow the persistor pattern
- Define keys as enum with String raw values - Use kebab-case for key names
- Use KeyValueStoring protocol - Not direct UserDefaults access
- Computed properties with get/set - Handle storage operations in accessors
- Use try? for error handling - KeyValueStore operations can throw
- Provide default values - Use nil coalescing operator (??) for defaults
- Inject KeyValueStore in init - Enable dependency injection and testing
Advanced Pattern for Optional Values
// ✅ 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
// ✅ 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:
// ❌ 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:
- Create a new persistor struct - Following the naming convention
*UserDefaultsPersistor - Define keys enum - Convert string keys to enum cases
- Convert properties - Transform @UserDefaultsWrapper properties to computed properties
- Update injection - Pass KeyValueStore through dependency injection
- Preserve key names - Ensure existing UserDefaults keys remain unchanged
Testing Pattern
// ✅ 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
// ❌ 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.