From 08256750f2c3280d3e13586822e2f6c0d8f9212d Mon Sep 17 00:00:00 2001 From: Mohamad Kredly <90929807+Mohamad-Kredly@users.noreply.github.com> Date: Sun, 6 Jul 2025 08:22:04 -0400 Subject: [PATCH 1/4] Added @TinyStorageItemSet property wrapper: Allows you to get all values for the keys matching the specified key type in a specified storage, as a dictionary of [Key: ValueType] - New protocol TinyStorageBuildableKey to specify that a type could be rebuilt from a string raw value - Value type must be equatable as to identify what changed in the dictionary, on set * String as a key is not supported as it would fetch all keys which goes against the use case of this propety wrapper --- Sources/TinyStorage/TinyStorage.swift | 58 +++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/Sources/TinyStorage/TinyStorage.swift b/Sources/TinyStorage/TinyStorage.swift index 75c4a4b..d38ec03 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] = storage.retrieve(type: T.self, forKey: key) ?? (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) + } + } +} From 97fe1214e7787a6b330454547b4749479fd502c9 Mon Sep 17 00:00:00 2001 From: Mohamad Kredly <90929807+Mohamad-Kredly@users.noreply.github.com> Date: Sun, 6 Jul 2025 09:12:04 -0400 Subject: [PATCH 2/4] Updated README to include @TinyStorageItemSet documentation. Moved "Migrating from UserDefaults" to its own section --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dca2ee3..091f583 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)) { 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"] From f96c9628fafcd6c271af5a998ce6ef6721ab8044 Mon Sep 17 00:00:00 2001 From: Mohamad Kredly <90929807+Mohamad-Kredly@users.noreply.github.com> Date: Sun, 6 Jul 2025 09:43:59 -0400 Subject: [PATCH 3/4] README: Fixed missing Id in the example TinyStorageItemSet's ForEach --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 091f583..bb09202 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ var platformStates: [Platform: AuthorizationState] = [.discord: .signedOut] var body: some View { VStack { - ForEach(Array(platformStates.keys)) { platform in + ForEach(Array(platformStates.keys), id: \.rawValue) { platform in Text("Platform \(platform.rawValue) is \(platformStates[platform]?.rawValue ?? "unset")") } } From 51ec5a62d87cd5ac9856bceebde4b385a90bc235 Mon Sep 17 00:00:00 2001 From: Mohamad Kredly <90929807+Mohamad-Kredly@users.noreply.github.com> Date: Sun, 6 Jul 2025 10:05:50 -0400 Subject: [PATCH 4/4] TinyStorageItemSet/get: No longer calls storage.retrieve() twice --- Sources/TinyStorage/TinyStorage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/TinyStorage/TinyStorage.swift b/Sources/TinyStorage/TinyStorage.swift index d38ec03..92764c7 100644 --- a/Sources/TinyStorage/TinyStorage.swift +++ b/Sources/TinyStorage/TinyStorage.swift @@ -801,7 +801,7 @@ public struct TinyStorageItemSet