Skip to content

Releases: narek-sv/KeyValueStorage

v3.0.0

Choose a tag to compare

@narek-sv narek-sv released this 15 Oct 19:31

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 KeyValueStorage service instead of the bundle identifier, and items with a domain under KeyValueStorage.<access group> or Domain.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/Files and <group container>/KeyValueStorage/Files, instead of Documents/<bundle identifier> and <group container>/<bundle identifier>.
  • Files: % and characters that change with case, such as uppercase letters, are percent-encoded in file names, so Key is stored as %4Bey, and keys that differ only in case are separate values on every platform.
  • Files: invalid keys throw FileStorage.Error.invalidKey. failedToSave is removed, failures are reported as .other, and failedToFindDocumentsDirectory is renamed to failedToFindApplicationSupportDirectory.
  • Coding: the default coder is RawDataCoder, which stores Data as is and String as UTF-8 instead of JSON.
  • Coding: XMLDataCoder is renamed to PropertyListDataCoder and stores every value as the only element of an array.
  • Coding: JSONDataCoder and PropertyListDataCoder are structs instead of actors.
  • Coding: fetched values are decoded as the key's value type instead of its optional, so a stored JSON null for a non-optional key throws instead of returning nil.
  • UnifiedStorage: clear() and clear(storage:) are removed. Use clear(storage:forDomain:), or delete the keys.
  • Observation: publishers and streams start with the current value. Pass includingCurrentValue: false for the previous behavior.
  • Observation: publisher(forKey:) returns KeyValuePublisher instead of AnyPublisher, and KeyValueObservableStorage's publisher(forKey:) and stream(forKey:) are async throws.
  • Concurrency: KeyValueCodingStorage is no longer isolated to CodingStorageActor, so creating it doesn't need await.
  • Concurrency: the library no longer declares UserDefaults, AnyPublisher, AsyncPublisher and AnyCancellable as @unchecked Sendable for your whole app.

Fixed

  • DefaultUnifiedStorageFactory and ObservableUnifiedStorageFactory couldn'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 UserDefaultsStorage created with an injected UserDefaults wiped the standard domain instead of the injected one.
  • Concurrent first accesses to a storage through UnifiedStorage created one storage per caller.
  • PropertyListDataCoder couldn't encode single values such as String or Int.
  • Observations were never released, even after all subscribers were gone.

Added

  • SQLiteStorage and SQLiteKey, 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:) and ObservableUnifiedStorageFactory(coder:).
  • includingCurrentValue for publisher(forKey:) and stream(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 public KeychainWrapper to use custom services.
  • FileStorage.directoryName and SQLiteStorage.directoryName, the names of their folders in KeyValueStorage.
  • The storages' domain can be read without await.
  • visionOS 1.0 or later is a supported platform.

Performance

  • Fetching and saving through UnifiedStorage is about 23% faster, because key types are hashed with ObjectIdentifier instead 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 Data value 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.

v2.1.0

Choose a tag to compare

@narek-sv narek-sv released this 03 Apr 13:46
4331fbb
  • Add Privacy Manifest
  • Fix new Swift 5.10 strict concurrency warnings (which will be compile error in upcoming Swift 6)

v2.0.0

Choose a tag to compare

@narek-sv narek-sv released this 03 Mar 18:53

Introducing the new version of our framework v2.0.0.

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.


Breaking changes:

  • The framework has been rewritten entirely, and the old one is deprecated. Do not update if you do not intend to use the new one.

v1.1.0

Choose a tag to compare

@narek-sv narek-sv released this 10 Dec 14:51
  • Added new property wrapper for easy usage
  • Fixed a bug when clear method deletes all in memory storage regardless accessGroup

v1.0.2

Choose a tag to compare

@narek-sv narek-sv released this 20 Aug 13:34
1fff0a5

Bug fixes

v1.0.1

Choose a tag to compare

@narek-sv narek-sv released this 20 Aug 13:03
43e7e49
  • Updated deployment targets
  • Added CocoaPods support

v1.0.0

Choose a tag to compare

@narek-sv narek-sv released this 20 Aug 11:49
553d820

Initial release

v1.0.0-beta-3

v1.0.0-beta-3 Pre-release
Pre-release

Choose a tag to compare

@narek-sv narek-sv released this 18 Aug 23:09

Revised the .inMemory storage type and improved the documentation

1.0.0-beta-2

1.0.0-beta-2 Pre-release
Pre-release

Choose a tag to compare

@narek-sv narek-sv released this 29 Jul 08:14
30f6608

Added additional options for .keychain and .inMemory storage types.

Keychain

  • accessibility: Optional accessibility to use when retrieving the keychain item.
  • synchronizable: Optional bool that describes if the item should be synchronizable, to be synched with the iCloud.

InMemory

  • static: Optional bool that describes if the item should be stored in static storage.

1.0.0-beta-1

1.0.0-beta-1 Pre-release
Pre-release

Choose a tag to compare

@narek-sv narek-sv released this 27 Jul 13:02
30f6608

Initial beta release