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 = .nowKeyValueStorage 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.
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 + 1You 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.personAlso, 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.accountKeyGroup 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.showConsoleYou 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.