diff --git a/Sources/Sharing/SharedKeys/AppStorageKey.swift b/Sources/Sharing/SharedKeys/AppStorageKey.swift index cb61de5..6996e73 100644 --- a/Sources/Sharing/SharedKeys/AppStorageKey.swift +++ b/Sources/Sharing/SharedKeys/AppStorageKey.swift @@ -96,6 +96,20 @@ where Self == AppStorageKey { AppStorageKey(key, store: store) } + + /// Creates a shared key that can read and write a Codable value to user defaults. + /// + /// - Parameters: + /// - key: The key to read and write the value to in the user defaults store. + /// - store: The user defaults store to read and write to. A value of `nil` will use the user + /// default store from dependencies. + /// - Returns: A user defaults shared key. + public static func appStorage( + _ key: String, store: UserDefaults? = nil + ) -> Self + where Self == AppStorageKey { + AppStorageKey(key, store: store) + } /// Creates a shared key that can read and write to an integer user default, transforming /// that to a `RawRepresentable` data type. @@ -210,6 +224,20 @@ where Self == AppStorageKey { AppStorageKey(key, store: store) } + + /// Creates a shared key that can read and write a Codable value to user defaults. + /// + /// - Parameters: + /// - key: The key to read and write the value to in the user defaults store. + /// - store: The user defaults store to read and write to. A value of `nil` will use the user + /// default store from dependencies. + /// - Returns: A user defaults shared key. + public static func appStorage( + _ key: String, store: UserDefaults? = nil + ) -> Self + where Self == AppStorageKey { + AppStorageKey(key, store: store) + } /// Creates a shared key that can read and write to an optional integer user default, /// transforming that to a `RawRepresentable` data type. @@ -317,6 +345,10 @@ fileprivate init(_ key: String, store: UserDefaults?) where Value == Date { self.init(lookup: CastableLookup(), key: key, store: store) } + + fileprivate init(_ key: String, store: UserDefaults?) where Value: Codable { + self.init(lookup: CodableLookup(), key: key, store: store) + } fileprivate init(_ key: String, store: UserDefaults?) where Value: RawRepresentable { self.init(lookup: RawRepresentableLookup(base: CastableLookup()), key: key, store: store) @@ -353,6 +385,15 @@ fileprivate init(_ key: String, store: UserDefaults?) where Value == Date? { self.init(lookup: OptionalLookup(base: CastableLookup()), key: key, store: store) } + + fileprivate init(_ key: String, store: UserDefaults?) + where Value == C? { + self.init( + lookup: OptionalLookup(base: CodableLookup()), + key: key, + store: store + ) + } fileprivate init>(_ key: String, store: UserDefaults?) where Value == R? { @@ -602,6 +643,35 @@ } } + private struct CodableLookup: Lookup { + func loadValue( + from store: UserDefaults, + at key: String, + default defaultValue: Value? + ) -> Value? { + guard let data = store.data(forKey: key) + else { + guard !SharedAppStorageLocals.isSetting + else { return nil } + SharedAppStorageLocals.$isSetting.withValue(true) { + if let value = defaultValue, let encoded = try? JSONEncoder().encode(value) { + store.set(encoded, forKey: key) + } + } + return defaultValue + } + return (try? JSONDecoder().decode(Value.self, from: data)) ?? defaultValue + } + + func saveValue(_ newValue: Value, to store: UserDefaults, at key: String) { + SharedAppStorageLocals.$isSetting.withValue(true) { + if let encoded = try? JSONEncoder().encode(newValue) { + store.set(encoded, forKey: key) + } + } + } + } + private struct URLLookup: Lookup { typealias Value = URL diff --git a/Tests/SharingTests/AppStorageTests.swift b/Tests/SharingTests/AppStorageTests.swift index e377932..2db6fad 100644 --- a/Tests/SharingTests/AppStorageTests.swift +++ b/Tests/SharingTests/AppStorageTests.swift @@ -64,6 +64,18 @@ store.set(Date(timeIntervalSince1970: 0), forKey: "date") #expect(date == Date(timeIntervalSince1970: 0)) } + + @Test func codable() { + struct Item: Codable, Equatable { + var id: Int + } + @Shared(.appStorage("codable")) var item = Item(id: 42) + let data = store.data(forKey: "codable") ?? Data() + #expect((try? JSONDecoder().decode(Item.self, from: data)) == Item(id: 42)) + let encoded = try? JSONEncoder().encode(Item(id: 1729)) + store.set(encoded, forKey: "codable") + #expect(item == Item(id: 1729)) + } @Test func rawRepresentableInt() { struct ID: RawRepresentable {