Files
obsidian-vault/work/wiki/apple-browsers/user-defaults-storage.md
T

218 lines
7.2 KiB
Markdown

---
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.