mtj0928/key-value-storage
A type-safe, observable, and injectable wrapper of UserDefaults.
Simple Example
- Define keys and types you want to save.
struct AppKeys: KeyGroup {
let launchCount = KeyDefinition(key: "launchCount", defaultValue: 0)
let lastLaunchDate = KeyDefinition<Date?>(key: "lastLaunchDate")
}- Make a storage and read / write the value by
@dynamicMemberLookup
let storage = KeyValueStorage<AppKeys>(backend: UserDefaults.standard)
// Read
let launchCount = storage.launchCount
// Write
storage.launchCount = launchCount + 1
strorage.lastLaunchDate = .nowConcepts
KeyValueStorage is developed based on the following three concepts:
- Type-safety: You can read and write common types such as
IntandStringand your custom types in a type-safe manner. - Injectable Backend: You can easily change the backend storage where values are stored to any
UserDefaultsorInMemoryStorage. - Observable Changes:
KeyValueStoragesupports Observation, AsyncSequence, and Publisher of Combine.
Type-safety
Key Definitions
As shown in the above section, defining keys in a key group makes your code type-safe.
struct AppKeys: KeyGroup {
let launchCount = KeyDefinition(key: "launchCount", defaultValue: 0)
let lastLaunchDate = KeyDefinition<Date?>(key: "lastLaunchDate")
}You can specify all types UserDefaults can accept to the type of KeyDefinition. If you specify Optional to the type of KeyDefinition like lastLaunchDate, you can omit the default value.
And you can read and write the value.
let storage = KeyValueStorage<AppKeys>(backend: UserDefaults.standard)
let lastLaunchDate: Int = storage.lastLaunchDate
storage.lastLaunchDate = lastLaunchDate + 1Custom Type Support
You can store and read your custom type by making the type conform to KeyValueStorageValue.
If your type is RawRepresentable, it's enough to add the conformance.
enum Fruit: Int, KeyValueStorageValue {
case apple
case banana
case orange
}
struct AppKeys: KeyGroup {
let fruit = KeyDefinition<Fruit>(key: "fruit", defaultValue: .apple)
}
let storage = KeyValueStorage<AppKeys>(backend: UserDefaults.standard)
let fruit: Fruit = storage.fruitIn other cases, you can write custom serialization / deserialization logics.
struct Person: KeyValueStorageValue, Equatable {
typealias StoredRawValue = [String: any Sendable]
var name: String
var age: Int
func serialize() -> StoredRawValue {
["name": name, "age": age]
}
static func deserialize(from dictionary: StoredRawValue) -> Person? {
guard let name = dictionary["name"] as? String,
let age = dictionary["age"] as? Int
else { return nil }
return Person(name: name, age: age)
}
}
struct AppKeys: KeyGroup {
let person = KeyDefinition<Person?>(key: "person")
}
let storage = KeyValueStorage<AppKeys>(backend: UserDefaults.standard)
let person: Person? = storage.personCodable Support (JSON)
Also, you can easily store your type inhering Codable by using JSONKeyDefinition.
struct Account: Codable {
var name: String
var email: String
}
struct AppKeys: KeyGroup {
let account = JSONKeyDefinition<Account?>(key: "account")
}
let storage = KeyValueStorage<AppKeys>(backend: UserDefaults.standard)
let account: Account? = storage.accountKey Group
KeyGroup is a combination of keys, and all keys in the same group are ensured to be stored in the same storage.
And, the group can be nested in another group.
So, for example, you can divide the keys by purpose and combine them into one group.
struct AppKeys: KeyGroup {
let launchCount = KeyDefinition(key: "launchCount", defaultValue: 0)
let debug = DebugKeys()
}
struct DebugKeys: KeyGroup {
let showConsole = KeyDefinition<Bool>(key: "showConsole", defaultValue: false)
}
let standardStorage = KeyValueStorage<AppKeys>(backend: UserDefaults.standard)
let launchCount = standardStorage.launchCount
let showConsole = standardStorage.debug.showConsoleInjectable Backend
You can easily change the backend where values are stored to any UserDefaults.
let standardStorage = KeyValueStorage<StandardKeys>(backend: UserDefaults.standard)
let appGroupStorage = KeyValueStorage<AppGroupKeys>(backend: UserDefaults(suiteName: "APP_GROUP")!)InMemoryStorage is also available as the backend. This is useful when you want to run unit tests in parallel.
let standardStorage = KeyValueStorage<StandardKeys>(backend: InMemoryStorage())Observable Changes
Observation
KeyValueStorage supports Observation by default.
For example, this view is automatically updated when the counter is updated.
struct Keys: KeyGroup {
let counter = KeyDefinition(key: "counter", defaultValue: 0)
}
struct ContentView: View {
var storage: KeyValueStorage<Keys>
var body: some View {
VStack {
Text("\(storage.counter)")
Button("add") {
storage.counter += 1
}
}
}
}[!NOTE] Please capture the KeyValueStorage for as long as you need to observe it, because the observation is finished when the KeyValueStorage is released.
AsyncSequence
You can observe the changes to key by AsyncSequence
let storage: KeyValueStorage<Keys> = ...
Task {
for await _ in storage.stream(key: \.counter) {
print("New value: \(storage.counter)")
}
}[!NOTE] Please capture the KeyValueStorage for as long as you need to observe it, because the stream is finished when the KeyValueStorage is released.
Combine
You can observe the changes to key by AsyncSequence
let storage: KeyValueStorage<Keys> = ...
storage.publishers(key: \.counter)
.sink {
print("New value: \(storage.counter)")
}[!NOTE] Please capture the KeyValueStorage for as long as you need to observe it, because the stream is finished when the KeyValueStorage is released.
Requirements
Swift 6+
Platforms
- iOS 17+
- macOS 14+
- watchOS 10+
- visionOS 1+
- tvOS 17+
Installation
You can add this package by Swift Package Manager.
dependencies: [
.package(url: "https://github.com/mtj0928/key-value-storage", from: "0.3.0")
],
targets: [
.target(name: "YOUR_TARGETS", dependencies: [
.product(name: "KeyValueStorage", package: "key-value-storage")
]),
]Documentations
Documentations including several articles are available here.
Package Metadata
Repository: mtj0928/key-value-storage
Default branch: main
README: README.md