Repository navigation
Version 3 fixes data loss, security and concurrency issues, which required changing where and how values are stored. Values saved by 2.x are not migrated automatically; see Migrating from 2.x.
Breaking changes
- Installation: CocoaPods is no longer supported. Add the package with Swift Package Manager.
- Installation: Swift 6.0 (Xcode 16) or later is required, and the library is compiled in the Swift 6 language mode.
- Keychain: items without a domain are saved under the
KeyValueStorageservice instead of the bundle identifier, and items with a domain underKeyValueStorage.<access group>orDomain.service. - Keychain: a key without accessibility saves its items as
.whenUnlocked, and every save applies the key's accessibility to the existing item. - Files: values are stored in
Application Support/<bundle identifier>/KeyValueStorage/Filesand<group container>/KeyValueStorage/Files, instead ofDocuments/<bundle identifier>and<group container>/<bundle identifier>. - Files:
%and characters that change with case, such as uppercase letters, are percent-encoded in file names, soKeyis stored as%4Bey, and keys that differ only in case are separate values on every platform. - Files: invalid keys throw
FileStorage.Error.invalidKey.failedToSaveis removed, failures are reported as.other, andfailedToFindDocumentsDirectoryis renamed tofailedToFindApplicationSupportDirectory. - Coding: the default coder is
RawDataCoder, which storesDataas is andStringas UTF-8 instead of JSON. - Coding:
XMLDataCoderis renamed toPropertyListDataCoderand stores every value as the only element of an array. - Coding:
JSONDataCoderandPropertyListDataCoderare structs instead of actors. - Coding: fetched values are decoded as the key's value type instead of its optional, so a stored JSON
nullfor a non-optional key throws instead of returningnil. - UnifiedStorage:
clear()andclear(storage:)are removed. Useclear(storage:forDomain:), or delete the keys. - Observation: publishers and streams start with the current value. Pass
includingCurrentValue: falsefor the previous behavior. - Observation:
publisher(forKey:)returnsKeyValuePublisherinstead ofAnyPublisher, andKeyValueObservableStorage'spublisher(forKey:)andstream(forKey:)areasync throws. - Concurrency:
KeyValueCodingStorageis no longer isolated toCodingStorageActor, so creating it doesn't needawait. - Concurrency: the library no longer declares
UserDefaults,AnyPublisher,AsyncPublisherandAnyCancellableas@unchecked Sendablefor your whole app.
Fixed
DefaultUnifiedStorageFactoryandObservableUnifiedStorageFactorycouldn't be created or subclassed outside the module, so observation couldn't be enabled.- App extensions couldn't read keychain items and files that the app saved to a shared domain.
- Changing a keychain key's accessibility made its item unreadable and every later save fail.
- Clearing the keychain left iCloud-synchronizable items, and on macOS searched a different keychain than the one items are saved to.
- An empty file key deleted every stored file and broke the storage,
../keys wrote outside the storage directory, and keys with/failed silently. - Saving a file wasn't atomic, read errors were returned as missing values, and clearing reported success when the directory couldn't be read.
- Clearing a
UserDefaultsStoragecreated with an injectedUserDefaultswiped the standard domain instead of the injected one. - Concurrent first accesses to a storage through
UnifiedStoragecreated one storage per caller. PropertyListDataCodercouldn't encode single values such asStringorInt.- Observations were never released, even after all subscribers were gone.
Added
SQLiteStorageandSQLiteKey, which keep all values of a domain in one SQLite database that an app and its extensions can use at the same time.RawDataCoder(fallback:), the new default coder.DefaultUnifiedStorageFactory(coder:)andObservableUnifiedStorageFactory(coder:).includingCurrentValueforpublisher(forKey:)andstream(forKey:).KeychainStorage.Domain(accessGroup:service:), which also accepts an App Group as the keychain access group,Domain(groupId:teamId:service:)for Keychain Sharing groups,KeychainStorage.defaultService, and a publicKeychainWrapperto use custom services.FileStorage.directoryNameandSQLiteStorage.directoryName, the names of their folders inKeyValueStorage.- The storages'
domaincan be read withoutawait. - visionOS 1.0 or later is a supported platform.
Performance
- Fetching and saving through
UnifiedStorageis about 23% faster, because key types are hashed withObjectIdentifierinstead of their names. - Coding no longer runs on shared actors: 8 tasks saving and fetching 50 KB values finish 3.6x faster than sequentially, instead of taking the same time.
- Saving and fetching a 100 KB
Datavalue is about 350x faster, and a 10 KB string about 11x faster.
Migrating from 2.x
Read the values from their 2.x locations with JSONDataCoder, save them with 3.0, and remove the old ones:
let storage = UnifiedStorage()
let bundleIdentifier = Bundle.main.bundleIdentifier!
// Keychain (iOS): 2.x used the bundle identifier as the service
let oldKeychain = KeyValueCodingStorage(storage: await KeychainStorage(keychain: KeychainWrapper(serviceName: bundleIdentifier)),
coder: JSONDataCoder())
let oldTokenKey = KeyValueCodingStorageKey<KeychainStorage, String>(key: .init(name: "token"))
if let token = try await oldKeychain.fetch(forKey: oldTokenKey) {
try await storage.save(token, forKey: KeychainKey<String>(key: .init(name: "token")))
try await oldKeychain.delete(forKey: oldTokenKey)
}
// Files: 2.x stored them in Documents/<bundle identifier>, named exactly like the key
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let oldAvatar = documents.appendingPathComponent(bundleIdentifier).appendingPathComponent("avatar")
if let data = try? Data(contentsOf: oldAvatar) {
let avatar: Data = try JSONDataCoder().decode(data)
try await storage.save(avatar, forKey: FileKey<Data>(key: "avatar"))
try FileManager.default.removeItem(at: oldAvatar)
}
// UserDefaults: the keys are the same, only String and Data values are encoded differently
let oldDefaults = UnifiedStorage(factory: DefaultUnifiedStorageFactory(coder: JSONDataCoder()))Keychain items saved by 2.x on macOS are in the file-based keychain, which 3.0 doesn't read.