Skip to content

๐Ÿ’พ Safe, statically-typed, store-agnostic key-value storage written in Swift!

License

Notifications You must be signed in to change notification settings

SwiftKitz/Storez

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

112 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Storez ๐Ÿ’พ

Safe, statically-typed, store-agnostic key-value storage

Version CI Swift Platforms

Highlights

  • Fully Customizable:
    Customize the persistence store, the KeyType class, post-commit actions .. Make this framework yours!

  • Batteries Included:
    In case you just want to use stuff, the framework is shipped with pre-configured basic set of classes that you can just use.

  • Portability, Check!:
    If you're looking to share code between your app and extensions, you're covered! Use UserDefaultsStore with a shared container suite name.

  • Swift 6 Ready:
    Full Swift 6 language mode with strict concurrency. All stores and keys are Sendable.

Example:

final class WeatherService {

  private enum Keys: Namespace {
    static let id = "weather-service"
    static let temperature = Key<Keys, Double>(id: "temp", defaultValue: 0.0)
  }

  private let store: UserDefaultsStore
  
  var temperature: Double {
    return store.get(Keys.temperature)
  }

  init(store: UserDefaultsStore) {
    self.store = store
  }

  func weatherUpdate(temperature: Double) {
    store.set(Keys.temperature, temperature)
  }
}

Features

Available Stores

Store Backend
UserDefaultsStore UserDefaults
CacheStore NSCache

Type-safe, store-agnostic, nestable Key definitions

// Entries must belong to a "group", for namespacing
struct Animals: Namespace {
    static let id = "animals"
}

let kingdom = Key<Animals, Void?>(id: "mammals", defaultValue: nil)
kingdom.stringValue // "animals:mammals"

// Nesting
struct Cats: Namespace {
    typealias parent = Animals
    static let id = "cats"

    // Namespaces also have pre and post commit hooks
    func preCommitHook() { /* custom code */ }
    func postCommitHook() { /* custom code */ }
}

let cat = Key<Cats, Void?>(id: "lion", defaultValue: nil)
cat.stringValue // "animals:cats:lion"

Initialize the store you want

// Use UserDefaultsStore for this example
let store = UserDefaultsStore(suite: "io.kitz.testing")
let key = Key<GlobalNamespace, Int?>(id: "key", defaultValue: nil)

// With three simple functions
store.set(key, value: 8)
store.get(key) // 8
store.clear() // Start fresh every time for testing

Optionality is honored throughout

let nullable = Key<GlobalNamespace, String?>(id: "nullable", defaultValue: nil)
store.get(nullable)?.isEmpty   // nil
store.set(nullable, value: "")
store.get(nullable)?.isEmpty   // true

let nonnull = Key<GlobalNamespace, String>(id: "nonnull", defaultValue: "!")
store.get(nonnull).isEmpty  // false
store.set(nonnull, value: "")
store.get(nonnull).isEmpty  // true

Custom objects easily supported via Codable

struct CustomObject: Codable {
    var strings: [String]
}

let customObject = CustomObject(
    strings: ["fill", "in", "the"]
)

// Add a processing block to transform values on set
let customKey = Key<GlobalNamespace, CustomObject?>(id: "custom", defaultValue: nil) {
    var processedValue = $0
    processedValue?.strings.append("blank!")
    return processedValue
}

store.set(customKey, value: customObject)
store.get(customKey)?.strings.joined(separator: " ") // fill in the blank!

You can also use UserDefaultsConvertible for custom serialization.

Make your own KeyType

// For example, make a key that posts notifications on change
struct MyKey<G: Namespace, V>: KeyType {

    typealias NamespaceType = G
    typealias ValueType = V

    var stringValue: String
    var defaultValue: ValueType

    func didChange(_ oldValue: ValueType, newValue: ValueType) {
        NotificationCenter.default.post(name: .init(stringValue), object: nil)
    }
}

Migrate from legacy keys

let migration = UserDefaultsStore.Migration(
    to: Key<MyNamespace, City>(id: "city", defaultValue: .default),
    from: "OldCityKey"
) { oldValue in
    // Convert the old raw value to the new type
    City(rawValue: oldValue as! String)
}

store.migrate([migration])

Getting Started

Swift Package Manager

You can add Storez to an Xcode project by adding it as a package dependency.

Depending on how your project is structured:

  • If you have a single application target that needs access to the library, then add Storez directly to your application.
  • If you want to use this library from multiple Xcode targets, or mixing Xcode targets and SPM targets, you likely want to create a shared framework that depends on Storez and then depend on that framework in all of your targets.

To use Storez in a Package.swift file, add this to the dependencies: section.

.package(url: "https://github.com/SwiftKitz/Storez.git", from: "5.0.0"),

Motivation

I've seen a lot of great attempts at statically-typed data stores, but they all build a tightly coupled design that limits the end-developer's freedom. With this framework, you can start prototyping right away with the shipped features, then replace the persistence store and KeyType functionality with your heart's content and keep your code the way it is!

Author

Mazyod (@Mazyod)

License

Storez is released under the MIT license. See LICENSE for details.

Packages

No packages published

Contributors 5

Languages