[2026-04-28] Add apple-browsers .cursor rules (43 files) + executor v2 context
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user