A type-safe, observable, and injectable wrapper of UserDefaults.
- 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 = .now
KeyValueStorage
is developed based on the following three concepts:
- Type-safety: You can read and write common types such as
Int
andString
and your custom types in a type-safe manner. - Injectable Backend: You can easily change the backend storage where values are stored to any
UserDefaults
orInMemoryStorage
. - Observable Changes:
KeyValueStorage
supports Observation, AsyncSequence, and Publisher of Combine.
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 + 1
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.fruit
In 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.person
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.account
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.showConsole
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())
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.
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.
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.
Swift 6+
- iOS 17+
- macOS 14+
- watchOS 10+
- visionOS 1+
- tvOS 17+
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 including several articles are available here.