Skip to content

Commit c8b021c

Browse files
committed
Fix issue #26 overload resolution and document storage rules
1 parent be5e70f commit c8b021c

6 files changed

Lines changed: 637 additions & 132 deletions

File tree

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,58 @@ class AppearanceSettings {
446446

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

449+
### Storage Resolution Rules (Important for Direct Key Access)
450+
451+
These rules apply to both `@ObservableDefaults` (`UserDefaults`) and `@ObservableCloud` (`NSUbiquitousKeyValueStore`).
452+
When a type matches multiple constraints, the implementation chooses the most specific path in this order:
453+
454+
1. `RawRepresentable & PropertyListValue & Codable`
455+
2. `RawRepresentable & PropertyListValue`
456+
3. `RawRepresentable` (where `RawValue` is a PropertyList-compatible type)
457+
4. `PropertyListValue & Codable`
458+
5. `PropertyListValue`
459+
6. `Codable` only (JSON `Data` path; intentionally lower priority)
460+
461+
#### Persisted Format by Type Combination
462+
463+
- `RawRepresentable`-based paths: persist `rawValue`.
464+
- Example: `String`/`Int` raw values are stored directly as `String`/`Int`.
465+
- `PropertyListValue` paths: persist the value directly as PropertyList-compatible objects.
466+
- `Codable`-only path: persist JSON-encoded `Data`.
467+
- Optional values:
468+
- non-`nil`: stored using the same rules above
469+
- `nil`: key is removed
470+
471+
#### Read Fallback for Compatibility
472+
473+
For `RawRepresentable & PropertyListValue` (including `RawRepresentable & PropertyListValue & Codable`):
474+
475+
- Read attempts `rawValue` format first.
476+
- If that fails, read falls back to direct `PropertyListValue` casting.
477+
478+
This fallback keeps older data readable when a property was previously persisted via direct PropertyList format and later evolved to a `RawRepresentable` type.
479+
480+
#### Consistency for Manual `UserDefaults` / Cloud Reads and Writes
481+
482+
If you also read/write these keys directly outside the macros, use the same format rules to avoid mismatches.
483+
484+
- Use `rawValue` for all `RawRepresentable`-based properties.
485+
- Use direct PropertyList values for PropertyList paths.
486+
- Use JSON `Data` only for `Codable`-only properties.
487+
- Key naming follows macro key resolution:
488+
- default: `prefix + propertyName`
489+
- custom key: `@DefaultsKey` / `@CloudKey`
490+
491+
Example (`UserDefaults`):
492+
493+
```swift
494+
// For RawRepresentable-backed property (rawValue: String)
495+
defaults.set(theme.rawValue, forKey: "app_theme")
496+
497+
// For Codable-only property
498+
defaults.set(try JSONEncoder().encode(profile), forKey: "app_profile")
499+
```
500+
449501
### Integrating with Other Observable Objects
450502

451503
It's recommended to manage storage data separately from your main application state:

README_zh.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,58 @@ class AppearanceSettings {
446446

447447
当类型同时遵循 `RawRepresentable``Codable` 时,库会优先使用 `RawRepresentable` 的存储方式,通过原始值(raw value)存储数据,而不是使用 JSON 编码。这确保了与现有数据的向后兼容性,并为枚举类型提供了更高效的存储方式。
448448

449+
### 存储决策规则(直接访问 Key 时请务必遵循)
450+
451+
以下规则同时适用于 `@ObservableDefaults``UserDefaults`)和 `@ObservableCloud``NSUbiquitousKeyValueStore`)。
452+
当类型同时满足多个约束时,按“越具体越优先”的顺序选择:
453+
454+
1. `RawRepresentable & PropertyListValue & Codable`
455+
2. `RawRepresentable & PropertyListValue`
456+
3. `RawRepresentable`(且 `RawValue` 为 PropertyList 可存储类型)
457+
4. `PropertyListValue & Codable`
458+
5. `PropertyListValue`
459+
6.`Codable`(JSON `Data` 路径,优先级最低)
460+
461+
#### 各组合的实际存储格式
462+
463+
- `RawRepresentable` 路径:保存 `rawValue`
464+
- 例如 `String`/`Int` rawValue 会直接以 `String`/`Int` 存储。
465+
- `PropertyListValue` 路径:直接以 PropertyList 值存储。
466+
-`Codable` 路径:以 JSON 编码后的 `Data` 存储。
467+
- Optional 值:
468+
-`nil`:按上述规则保存
469+
- `nil`:删除对应 key
470+
471+
#### 读取回退(兼容历史数据)
472+
473+
对于 `RawRepresentable & PropertyListValue`(包括 `RawRepresentable & PropertyListValue & Codable`):
474+
475+
- 读取时先按 `rawValue` 格式解析。
476+
- 若失败,再回退到直接 `PropertyListValue` 转换。
477+
478+
这保证了历史上“按 PropertyList 直接写入”的旧数据,在后来属性演进为 `RawRepresentable` 后仍可读取。
479+
480+
#### 与手动读写保持一致
481+
482+
如果你在其他位置直接读写 `UserDefaults` / iCloud key,请使用同样的格式规则:
483+
484+
- `RawRepresentable` 相关属性:手动写 `rawValue`
485+
- `PropertyListValue` 属性:手动写 PropertyList 原值
486+
-`Codable` 属性:手动写 JSON `Data`
487+
- key 规则与宏一致:
488+
- 默认:`prefix + propertyName`
489+
- 自定义 key:`@DefaultsKey` / `@CloudKey`
490+
491+
示例(`UserDefaults`):
492+
493+
```swift
494+
// RawRepresentable 属性(rawValue: String)
495+
defaults.set(theme.rawValue, forKey: "app_theme")
496+
497+
// 仅 Codable 属性
498+
defaults.set(try JSONEncoder().encode(profile), forKey: "app_profile")
499+
```
500+
449501
### 与其他 Observable 对象集成
450502

451503
建议将存储数据与主应用程序状态分开管理:

Sources/ObservableDefaults/NSUbiquitousKeyValueStore/NSUbiquitousKeyValueStoreWrapper.swift

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,92 @@ public struct NSUbiquitousKeyValueStoreWrapper: Sendable {
8282
return R(rawValue: rawValue) ?? defaultValue
8383
}
8484

85+
/// Gets a value for types that conform to both RawRepresentable and CloudPropertyListValue.
86+
///
87+
/// This dedicated overload removes ambiguity between the RawRepresentable and
88+
/// CloudPropertyListValue overload sets.
89+
public func getValue<Value>(
90+
_ key: String,
91+
_ defaultValue: Value) -> Value
92+
where Value: RawRepresentable, Value.RawValue: CloudPropertyListValue,
93+
Value: CloudPropertyListValue {
94+
// Prefer RawRepresentable storage for hybrid types.
95+
// Fallback to direct property-list casting to preserve compatibility with
96+
// legacy data written through non-raw paths.
97+
if let rawValue = store.object(forKey: key) as? Value.RawValue {
98+
return Value(rawValue: rawValue) ?? defaultValue
99+
}
100+
if let value = store.object(forKey: key) as? Value {
101+
return value
102+
}
103+
return defaultValue
104+
}
105+
106+
/// Gets an optional value for types that conform to both RawRepresentable and
107+
/// CloudPropertyListValue.
108+
///
109+
/// This dedicated overload removes ambiguity between the RawRepresentable and
110+
/// CloudPropertyListValue overload sets.
111+
public func getValue<R>(
112+
_ key: String,
113+
_ defaultValue: R?) -> R?
114+
where R: RawRepresentable, R.RawValue: CloudPropertyListValue, R: CloudPropertyListValue {
115+
// Prefer RawRepresentable storage for hybrid types.
116+
// Fallback to direct property-list casting to preserve compatibility with
117+
// legacy data written through non-raw paths.
118+
if let rawValue = store.object(forKey: key) as? R.RawValue {
119+
return R(rawValue: rawValue) ?? defaultValue
120+
}
121+
if let value = store.object(forKey: key) as? R {
122+
return value
123+
}
124+
return defaultValue
125+
}
126+
127+
/// Gets a value for types that conform to RawRepresentable,
128+
/// CloudPropertyListValue, and Codable.
129+
///
130+
/// This dedicated overload removes ambiguity with the
131+
/// CloudPropertyListValue & Codable overload set.
132+
public func getValue<Value>(
133+
_ key: String,
134+
_ defaultValue: Value) -> Value
135+
where Value: RawRepresentable, Value.RawValue: CloudPropertyListValue,
136+
Value: CloudPropertyListValue & Codable {
137+
// Prefer RawRepresentable storage for hybrid types.
138+
// Fallback to direct property-list casting to preserve compatibility with
139+
// legacy data written through non-raw paths.
140+
if let rawValue = store.object(forKey: key) as? Value.RawValue {
141+
return Value(rawValue: rawValue) ?? defaultValue
142+
}
143+
if let value = store.object(forKey: key) as? Value {
144+
return value
145+
}
146+
return defaultValue
147+
}
148+
149+
/// Gets an optional value for types that conform to RawRepresentable,
150+
/// CloudPropertyListValue, and Codable.
151+
///
152+
/// This dedicated overload removes ambiguity with the
153+
/// CloudPropertyListValue & Codable overload set.
154+
public func getValue<R>(
155+
_ key: String,
156+
_ defaultValue: R?) -> R?
157+
where R: RawRepresentable, R.RawValue: CloudPropertyListValue,
158+
R: CloudPropertyListValue & Codable {
159+
// Prefer RawRepresentable storage for hybrid types.
160+
// Fallback to direct property-list casting to preserve compatibility with
161+
// legacy data written through non-raw paths.
162+
if let rawValue = store.object(forKey: key) as? R.RawValue {
163+
return R(rawValue: rawValue) ?? defaultValue
164+
}
165+
if let value = store.object(forKey: key) as? R {
166+
return value
167+
}
168+
return defaultValue
169+
}
170+
85171
/// Gets a basic property list value from the ubiquitous key-value store.
86172
/// This method is used for basic types like String, Int, Bool, etc.
87173
/// - Parameters:
@@ -245,6 +331,68 @@ public struct NSUbiquitousKeyValueStoreWrapper: Sendable {
245331
}
246332
}
247333

334+
/// Sets a value for types that conform to both RawRepresentable and CloudPropertyListValue.
335+
///
336+
/// This dedicated overload removes ambiguity between the RawRepresentable and
337+
/// CloudPropertyListValue overload sets.
338+
public func setValue<Value>(
339+
_ key: String,
340+
_ newValue: Value)
341+
where Value: RawRepresentable, Value.RawValue: CloudPropertyListValue,
342+
Value: CloudPropertyListValue {
343+
// Prefer RawRepresentable storage for hybrid types.
344+
store.set(newValue.rawValue, forKey: key)
345+
}
346+
347+
/// Sets an optional value for types that conform to both RawRepresentable and
348+
/// CloudPropertyListValue.
349+
///
350+
/// This dedicated overload removes ambiguity between the RawRepresentable and
351+
/// CloudPropertyListValue overload sets.
352+
public func setValue<R>(
353+
_ key: String,
354+
_ newValue: R?)
355+
where R: RawRepresentable, R.RawValue: CloudPropertyListValue, R: CloudPropertyListValue {
356+
// Prefer RawRepresentable storage for hybrid types.
357+
if let newValue {
358+
store.set(newValue.rawValue, forKey: key)
359+
} else {
360+
store.removeObject(forKey: key)
361+
}
362+
}
363+
364+
/// Sets a value for types that conform to RawRepresentable,
365+
/// CloudPropertyListValue, and Codable.
366+
///
367+
/// This dedicated overload removes ambiguity with the
368+
/// CloudPropertyListValue & Codable overload set.
369+
public func setValue<Value>(
370+
_ key: String,
371+
_ newValue: Value)
372+
where Value: RawRepresentable, Value.RawValue: CloudPropertyListValue,
373+
Value: CloudPropertyListValue & Codable {
374+
// Prefer RawRepresentable storage for hybrid types.
375+
store.set(newValue.rawValue, forKey: key)
376+
}
377+
378+
/// Sets an optional value for types that conform to RawRepresentable,
379+
/// CloudPropertyListValue, and Codable.
380+
///
381+
/// This dedicated overload removes ambiguity with the
382+
/// CloudPropertyListValue & Codable overload set.
383+
public func setValue<R>(
384+
_ key: String,
385+
_ newValue: R?)
386+
where R: RawRepresentable, R.RawValue: CloudPropertyListValue,
387+
R: CloudPropertyListValue & Codable {
388+
// Prefer RawRepresentable storage for hybrid types.
389+
if let newValue {
390+
store.set(newValue.rawValue, forKey: key)
391+
} else {
392+
store.removeObject(forKey: key)
393+
}
394+
}
395+
248396
/// Sets a basic property list value in the ubiquitous key-value store.
249397
/// This method stores basic types like String, Int, Bool, etc.
250398
/// - Parameters:

0 commit comments

Comments
 (0)