Skip to content

Commit a541c5b

Browse files
authored
Add support for Codable types to AppStorageKey (#182)
1 parent 3f075e0 commit a541c5b

File tree

2 files changed

+82
-0
lines changed

2 files changed

+82
-0
lines changed

Sources/Sharing/SharedKeys/AppStorageKey.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@
9696
where Self == AppStorageKey<Date> {
9797
AppStorageKey(key, store: store)
9898
}
99+
100+
/// Creates a shared key that can read and write a Codable value to user defaults.
101+
///
102+
/// - Parameters:
103+
/// - key: The key to read and write the value to in the user defaults store.
104+
/// - store: The user defaults store to read and write to. A value of `nil` will use the user
105+
/// default store from dependencies.
106+
/// - Returns: A user defaults shared key.
107+
public static func appStorage<Value: Codable>(
108+
_ key: String, store: UserDefaults? = nil
109+
) -> Self
110+
where Self == AppStorageKey<Value> {
111+
AppStorageKey(key, store: store)
112+
}
99113

100114
/// Creates a shared key that can read and write to an integer user default, transforming
101115
/// that to a `RawRepresentable` data type.
@@ -210,6 +224,20 @@
210224
where Self == AppStorageKey<Date?> {
211225
AppStorageKey(key, store: store)
212226
}
227+
228+
/// Creates a shared key that can read and write a Codable value to user defaults.
229+
///
230+
/// - Parameters:
231+
/// - key: The key to read and write the value to in the user defaults store.
232+
/// - store: The user defaults store to read and write to. A value of `nil` will use the user
233+
/// default store from dependencies.
234+
/// - Returns: A user defaults shared key.
235+
public static func appStorage<Value: Codable>(
236+
_ key: String, store: UserDefaults? = nil
237+
) -> Self
238+
where Self == AppStorageKey<Value?> {
239+
AppStorageKey(key, store: store)
240+
}
213241

214242
/// Creates a shared key that can read and write to an optional integer user default,
215243
/// transforming that to a `RawRepresentable` data type.
@@ -317,6 +345,10 @@
317345
fileprivate init(_ key: String, store: UserDefaults?) where Value == Date {
318346
self.init(lookup: CastableLookup(), key: key, store: store)
319347
}
348+
349+
fileprivate init(_ key: String, store: UserDefaults?) where Value: Codable {
350+
self.init(lookup: CodableLookup(), key: key, store: store)
351+
}
320352

321353
fileprivate init(_ key: String, store: UserDefaults?) where Value: RawRepresentable<Int> {
322354
self.init(lookup: RawRepresentableLookup(base: CastableLookup()), key: key, store: store)
@@ -353,6 +385,15 @@
353385
fileprivate init(_ key: String, store: UserDefaults?) where Value == Date? {
354386
self.init(lookup: OptionalLookup(base: CastableLookup()), key: key, store: store)
355387
}
388+
389+
fileprivate init<C: Codable>(_ key: String, store: UserDefaults?)
390+
where Value == C? {
391+
self.init(
392+
lookup: OptionalLookup(base: CodableLookup()),
393+
key: key,
394+
store: store
395+
)
396+
}
356397

357398
fileprivate init<R: RawRepresentable<Int>>(_ key: String, store: UserDefaults?)
358399
where Value == R? {
@@ -602,6 +643,35 @@
602643
}
603644
}
604645

646+
private struct CodableLookup<Value: Codable & Sendable>: Lookup {
647+
func loadValue(
648+
from store: UserDefaults,
649+
at key: String,
650+
default defaultValue: Value?
651+
) -> Value? {
652+
guard let data = store.data(forKey: key)
653+
else {
654+
guard !SharedAppStorageLocals.isSetting
655+
else { return nil }
656+
SharedAppStorageLocals.$isSetting.withValue(true) {
657+
if let value = defaultValue, let encoded = try? JSONEncoder().encode(value) {
658+
store.set(encoded, forKey: key)
659+
}
660+
}
661+
return defaultValue
662+
}
663+
return (try? JSONDecoder().decode(Value.self, from: data)) ?? defaultValue
664+
}
665+
666+
func saveValue(_ newValue: Value, to store: UserDefaults, at key: String) {
667+
SharedAppStorageLocals.$isSetting.withValue(true) {
668+
if let encoded = try? JSONEncoder().encode(newValue) {
669+
store.set(encoded, forKey: key)
670+
}
671+
}
672+
}
673+
}
674+
605675
private struct URLLookup: Lookup {
606676
typealias Value = URL
607677

Tests/SharingTests/AppStorageTests.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@
6464
store.set(Date(timeIntervalSince1970: 0), forKey: "date")
6565
#expect(date == Date(timeIntervalSince1970: 0))
6666
}
67+
68+
@Test func codable() {
69+
struct Item: Codable, Equatable {
70+
var id: Int
71+
}
72+
@Shared(.appStorage("codable")) var item = Item(id: 42)
73+
let data = store.data(forKey: "codable") ?? Data()
74+
#expect((try? JSONDecoder().decode(Item.self, from: data)) == Item(id: 42))
75+
let encoded = try? JSONEncoder().encode(Item(id: 1729))
76+
store.set(encoded, forKey: "codable")
77+
#expect(item == Item(id: 1729))
78+
}
6779

6880
@Test func rawRepresentableInt() {
6981
struct ID: RawRepresentable {

0 commit comments

Comments
 (0)