diff --git a/README.md b/README.md index dca2ee3..bb09202 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,26 @@ Or better support for optional values: var nickname: String? = nil // or "Cool Guy" ``` -You can also migrate from a `UserDefaults` instance to `TinyStorage` with a handy helper function: +And, if you want to get all values of a specific key type in your storage, you can use the `@TinyStorageItemSet` property wrapper. As opposed to `@TinyStorageItem`, you specify your storage then initialize a dictionary having the key and value types of your choosing on the condition that: 1. The key conforms to `TinyStorageBuildableKey`, and 2. The value conforms to Equatable. Default values are supported the same as before, keys without a default would default to the value of the first key if available. + +Note that `String` is not supported as a key type for this property wrapper. + +```swift +@TinyStorageItemSet(storage: .appGroup) +var platformStates: [Platform: AuthorizationState] = [.discord: .signedOut] + +var body: some View { + VStack { + ForEach(Array(platformStates.keys), id: \.rawValue) { platform in + Text("Platform \(platform.rawValue) is \(platformStates[platform]?.rawValue ?? "unset")") + } + } +} +``` + +## Migrating from UserDefaults + +You can migrate from a `UserDefaults` instance to `TinyStorage` with a handy helper function: ```swift let nonBoolKeysToMigrate = ["favoriteIceCream", "appFontSize", "lastFetchDate"] diff --git a/Sources/TinyStorage/TinyStorage.swift b/Sources/TinyStorage/TinyStorage.swift index 75c4a4b..92764c7 100644 --- a/Sources/TinyStorage/TinyStorage.swift +++ b/Sources/TinyStorage/TinyStorage.swift @@ -750,6 +750,11 @@ public protocol TinyStorageKey: Hashable, Sendable { var rawValue: String { get } } +/// A ``TinyStorageKey`` that could be initialized from a String raw value +public protocol TinyStorageBuildableKey: TinyStorageKey { + init?(rawValue: String) +} + extension String: TinyStorageKey { public var rawValue: String { self } } @@ -779,3 +784,56 @@ public struct TinyStorageItem: DynamicProperty, Sendable ) } } + +@propertyWrapper +public struct TinyStorageItemSet: DynamicProperty, Sendable { + @State private var storage: TinyStorage + + private let defaultValue: [K: T] + + public init(wrappedValue: [K: T], storage: TinyStorage) { + self.defaultValue = wrappedValue + self.storage = storage + } + + public var wrappedValue: [K: T] { + get { + var itemDictionary: [K: T] = [:] + for key in storage.allKeys.compactMap({ K(rawValue: $0.rawValue) }) { + let retrieved = storage.retrieve(type: T.self, forKey: key) + itemDictionary[key] = retrieved ?? (defaultValue[key] ?? defaultValue.values.first) + } + if itemDictionary.isEmpty { + itemDictionary = defaultValue + } + return itemDictionary + } + nonmutating set { didSet(newValue) } + } + + public var projectedValue: Binding<[K: T]> { + Binding( + get: { wrappedValue }, + set: { didSet($0) } + ) + } + + private func didSet(_ newValue: [K: T]) { + let oldValue = wrappedValue + guard !(oldValue.isEmpty && newValue.isEmpty) else { return } + + let allKeys = Set(oldValue.keys).union(newValue.keys) + + let changedKeys = allKeys.filter { oldValue[$0] != newValue[$0] } + + let changedDict = newValue.filter({ changedKeys.contains($0.key) }) + for (key, value) in changedDict { + storage.store(value, forKey: key) + } + + let removedKeys = changedKeys.filter({ !changedDict.keys.contains($0) }) + for key in removedKeys { + storage.remove(key: key) + } + } +}