Enhance your development with the state-of-the-art key-value storage framework, meticulously designed for speed, safety, and simplicity. Leveraging Swift's advanced error handling and concurrency features, the framework ensures thread-safe interactions, bolstered by a robust, modular, and protocol-oriented architecture. Unique to the solution, types of values are encoded within the keys, enabling compile-time type inference and eliminating the need for unnecessary casting. It is designed with App Groups in mind, facilitating seamless data sharing between your apps and extensions. Experience a testable, easily integrated storage solution that redefines efficiency and ease of use.
iOS | macOS | watchOS | tvOS |
13.0+ | 10.15+ | 6.0+ | 13.0+ |
In Memory | User Defaults | Keychain | File System |
KeyValueStorage
also supports working with shared containers, which allows you to share your items among different App Extensions or your other Apps. To do so, first, you need to configure your app by following the steps described in this article.
By providing corresponding domain
s to each type of storage, you can enable the sharing of storage spaces. Alternatively, by doing so, you can also keep the containers isolated.
The framework is capable of working with any type that conforms to Codable
and Sendable
.
The concept here is that first you need to declare the key. It contains every piece of information about how and where the value is stored.
First, you need to declare the key. You can use one of the built-in types:
UserDefaultsKey
KeychainKey
InMemoryKey
FileKey
or you can define your own ones. See how to do that
import KeyValueStorage
let key = UserDefaultsKey<String>(key: "myKey")
// or alternatively provide the domain
let otherKey = UserDefaultsKey<String>(key: "myKey", domain: "sharedContainer")
As you can see, the key holds all the necessary information about the value:
- The key name -
"myKey"
- The storage type -
UserDefaults
- The value type -
String
- The domain (optional) -
"sharedContainer"
Now all that is left is to instantiate the storage and use it:
// Instantiate the storage
let storage = UnifiedStorage()
// Saves the item and associates it with the key,
// or overrides the value if there is already such an item
try await storage.save("Alice", forKey: key)
// Returns the item associated with the key or returns nil if there is no such item
let value = try await storage.fetch(forKey: key)
// Deletes the item associated with the key or does nothing if there is no such item
try await storage.delete(forKey: key)
// Sets the item identified by the key to the provided value
try await storage.set("Bob", forKey: key) // save
try await storage.set(nil, forKey: key) // delete
// Clears only the storage associated with the specified storage and domain
try await storage.clear(storage: InMemoryStorage.self, forDomain: "someDomain")
// Clears only the storage associated with the specified storage for all domains
try await storage.clear(storage: InMemoryStorage.self)
// Clears the whole storage content
try await storage.clear()
The framework leverages the full capabilities of Swift Generics, so it can infer the types of values based on the key compile-time, eliminating the need for extra checks or type casting.
struct MyType: Codable, Sendable { ... }
let key = UserDefaultsKey<MyType>(key: "myKey")
let value = try await storage.fetch(forKey: key) // inferred type for value is MyType
try await storage.save(/* accepts only MyType*/, forKey: key)
UnifiedStorage
has 4 built-in storage types:
In-memory
- This storage type persists the items only within an app session.User-Defaults
- This storage type persists the items within the app's lifetime.File-System
- This storage saves your key-values as separate files in your file system.Keychain
- This storage type keeps the items in secure storage and persists even after app re-installations. SupportsiCloud
synchronization.
You can also define your own storage, and it will work with it seamlessly with UnifiedStorage
out of the box.
All you need to do is:
- Define your own type that conforms to the
KeyValueDataStorage
protocol:
class NewStorage: KeyValueDataStorage { ... }
- Define the new key type (optional, for ease of use):
typealias NewStorageKey<Value: CodingValue> = UnifiedStorageKey<NewStorage, Value>
That's it. You can use it now as the built-in storages:
let key = NewStorageKey<UUID>(key: customKey)
try await storage.save(UUID(), forKey: key)
NOTE! You need to handle the thread safety of your storage on your own.
To get the advantages of Xcode autocompletion, it is recommended to declare all your keys in the extension of the UnifiedStorageKey
, like this:
extension UnifiedStorageKey {
static var key1: UserDefaultsKey<Int> {
.init(key: "key1", domain: nil)
}
static var key2: InMemoryKey<Date> {
.init(key: "key2", domain: "sharedContainer")
}
static var key3: KeychainKey<Double> {
.init(key: .init(name: "key3", accessibility: .afterFirstUnlock, isSynchronizable: true),
domain: .init(groupId: "groupId", teamId: "teamId"))
}
static var key4: FileKey<UUID> {
.init(key: "key4", domain: "otherContainer")
}
}
then Xcode will suggest all the keys specified in the extension when you put a dot:
Use accessibility
parameter to specify the security level of the keychain storage.
By default the .whenUnlocked
option is used. It is one of the most restrictive options and provides good data protection.
You can use .afterFirstUnlock
if you need your app to access the keychain item while in the background. Note that it is less secure than the .whenUnlocked
option.
Here are all the supported accessibility types:
afterFirstUnlock
afterFirstUnlockThisDeviceOnly
whenPasscodeSetThisDeviceOnly
whenUnlocked
whenUnlockedThisDeviceOnly
Set synchronizable
property to true
to enable keychain items synchronization across user's multiple devices. The synchronization will work for users who have the Keychain enabled in the iCloud settings on their devices. Deleting a synchronizable item will remove it from all devices.
let key = KeychainKey<String>(key: .init(name: "key", accessibility: .afterFirstUnlock, isSynchronizable: true),
domain: .init(groupId: "groupId", teamId: "teamId"))
The UnifiedStorage
initializer takes a factory
parameter that conforms to the UnifiedStorageFactory
protocol, enabling customized storage instantiation and configuration. This feature is particularly valuable for mocking storage in tests or substituting default implementations with custom ones.
By default, this parameter is set to DefaultUnifiedStorageFactory
, which omits observation capabilities to avoid excessive class burden. However, supplying an ObservableUnifiedStorageFactory
instance as the parameter activates observation of all underlying storages for changes.
Combine style publishers:
let key = InMemoryKey<String>(key: "key")
guard let publisher = try await storage.publisher(forKey: key) else {
// The storage is not properly configured
return
}
let subscription = publisher.sink { value in
print(value) // String?
}
Concurrency style async streams:
guard let stream = try await storage.stream(forKey: key) else {
// The storage is not properly configured
return
}
for await value in stream {
print(value) // String?
}
However, it's important to note that UnifiedStorage
can only observe changes made through its own methods.
Despite the fact that all the methods of the UnifiedStorage
are throwing, it will never throw an exception if you do all the initial setups correctly.
All built-in types leverage the power of Swift Concurrency and are thread-safe and protected from race conditions and data racing. However, if you extend the storage with your own ones, it is your responsibility to make them thread-safe.
The whole framework is thoroughly validated with high-quality unit tests. Additionally, it serves as an excellent demonstration of how to use the framework as intended.
Once you have your Swift package set up, adding KeyValueStorage as a dependency is as easy as adding it to the dependencies
value of your Package.swift
:
dependencies: [
.package(url: "https://github.com/narek-sv/KeyValueStorage.git", .upToNextMajor(from: "2.0.0"))
]
or
- In Xcode select File > Add Packages.
- Enter the project's URL: https://github.com/narek-sv/KeyValueStorage.git
In any file you'd like to use the package in, don't forget to import the framework:
import KeyValueStorage
To integrate KeyValueStorage into your Xcode project using CocoaPods, specify it in your Podfile
:
pod 'KeyValueStorageSwift'
Then run pod install
.
In any file you'd like to use the package in, don't forget to import the framework:
import KeyValueStorageSwift
See License.md for more information.