Coder Social home page Coder Social logo

josephduffy / persist Goto Github PK

View Code? Open in Web Editor NEW
44.0 3.0 3.0 3.6 MB

Extensible typesafe storage utilising property wrappers. Supports transformers such as Codable. Built in support for UserDefaults, NSUbiquitousKeyValueStore, FileManager, and in memory storage.

Home Page: https://josephduffy.github.io/Persist/

License: MIT License

Swift 99.99% Ruby 0.01%
swift swiftpm property-wrapper userdefaults nsubiquitouskeyvaluestore

persist's Introduction

Persist

Tests Supported Xcode Versions codecov Documentation SwiftPM Compatible

Persist is a framework that aids with persisting and retrieving values, with support for transformations such as storing as JSON data.

Usage

Persist provides the Persister class, which can be used to persist and retrieve values from various forms of storage.

The Persisted property wrapper wraps a Persister, making it easy to have a property that automatically persists its value.

class Foo {
    enum Bar: Int {
        case firstBar = 1
        case secondBar = 2
    }

    @Persisted(key: "foo-bar", userDefaults: .standard, transformer: RawRepresentableTransformer())
    var bar: Bar = .firstBar

    @Persisted(key: "foo-baz", userDefaults: .standard)
    var baz: String?
}

let foo = Foo()

foo.bar // "Bar.firstBar"
foo.bar = .secondBar
UserDefaults.standard.object(forKey: "foo-bar") // 2

foo.baz // nil
foo.baz = "new-value"
UserDefaults.standard.object(forKey: "foo-baz") // "new-value"

Persist includes out of the box support for:

  • UserDefaults
  • NSUbiquitousKeyValueStore
  • FileManager
  • InMemoryStorage (a simple wrapper around a dictionary)

Catching Errors

Persister's persist(_:) and retrieveValueOrThrow() functions will throw if the storage or transformer throws an error.

Persisted wraps a Persister and exposes it as the projectedValue, which allows you to catch errors:

class Foo {
    @Persisted(key: "foo-bar", userDefaults: .standard)
    var bar: String?
}

do {
    let foo = Foo()
    try foo.$bar.persist("new-value")
    try foo.$bar.retrieveValueOrThrow()
} catch {
    // Something went wrong
}

Subscribing to Updates

When targeting macOS 10.15, iOS 13, tvOS 13, or watchOS 6 or greater Combine can be used to subscribe to updates:

class Foo {
    @Persisted(key: "foo-bar", userDefaults: .standard)
    var bar: String?
}

let foo = Foo()
let subscription = foo.$bar.updatesPublisher.sink { result in
    switch result {
    case .success(let update):
        print("New value:", update.newValue)

        switch update.event {
        case .persisted(let newValue):
            print("Value updated to:", newValue)
            // `update.newValue` will be new value
        case .removed:
            print("Value was deleted")
            // `update.newValue` will be default value
        }
    case .failure(let error):
        print("Error occurred retrieving value after update:", error)
    }
}

For versions prior to macOS 10.15, iOS 13, tvOS 13, or watchOS 6 a closure API is provided:

class Foo {
    @Persisted(key: "foo-bar", userDefaults: .standard)
    var bar: String?
}

let foo = Foo()
let subscription = foo.$bar.addUpdateListener() { result in
    switch result {
    case .success(let update):
        print("New value:", update.newValue)

        switch update.event {
        case .persisted(let newValue):
            print("Value updated to:", newValue)
            // `update.newValue` will be new value
        case .removed:
            print("Value was deleted")
            // `update.newValue` will be default value
        }
    case .failure(let error):
        print("Error occurred retrieving value after update:", error)
    }
}

Transformers

Some storage methods will only support a subset of types, or you might want to modify how some values are encoded/decoded (e.g. to ensure on-disk date representation are the same as what an API sends/expects). This is where transformers come in:

struct Bar: Codable {
    var baz: String
}

class Foo {
    @Persisted(key: "bar", userDefaults: .standard, transformer: JSONTransformer())
    var bar: Bar?
}

let foo = Foo()
let subscription = foo.$bar.addUpdateListener() { result in
    switch result {
    case .success(let update):
        // `update.newValue` is a `Bar?`
        print("New value:", update.newValue)

        switch update.event {
        case .persisted(let bar):
            // `bar` is the decoded `Bar`
            print("Value updated to:", bar)
        case .removed:
            print("Value was deleted")
        }
    case .failure(let error):
        print("Error occurred retrieving value after update:", error)
    }
}

Transformers are typesafe, e.g. JSONTransformer is only usable when the value to be stored is Codable and the Storage supports Data.

Chaining Transformers

If a value should go through multiple transformers you can chain them.

struct Bar: Codable {
    var baz: String
}

public struct BarTransformer: Transformer {

    public func transformValue(_ bar: Bar) -> Bar {
        var bar = bar
        bar.baz = "transformed"
        return bar
    }

    public func untransformValue(_ bar: Bar) -> Bar {
        return bar
    }

}

class Foo {
    @Persisted(key: "bar", userDefaults: .standard, transformer: BarTransformer().append(JSONTransformer()))
    var bar: Bar?
}

let foo = Foo()
let bar = Bar(baz: "example value")
foo.bar = bar
foo.bar.baz // "transformed"

Default Values

A default value may be provided that will be used when the persister returns nil or throws and error.

struct Foo {
    @Persisted(key: "bar", userDefaults: .standard)
    var bar = "default"
}

var foo = Foo()
foo.bar // "default"

When provided as the defaultValue parameter the value is evaluated lazily when first required.

func makeUUID() -> UUID {
    print("Making UUID")
    return UUID()
}

struct Foo {
    @Persisted(key: "bar", userDefaults: .standard, defaultValue: makeUUID())
    var bar: UUID
}

/**
 This would not print anything because the default value is never required.
 */
var foo = Foo()
foo.bar = UUID()

/**
 This would print "Making UUID" once.
 */
var foo = Foo()
let firstCall = foo.bar
let secondCall = foo.bar
firstCall == secondCall // true

The default value can be optionally stored when used, either due to an error or because the storage returned nil. This can be useful when the first value is random and should be persisted between app launches once initially created.

struct Foo {
    @Persisted(key: "persistedWhenNilInt", userDefaults: .standard, defaultValue: Int.random(in: 1...10), defaultValuePersistBehaviour: .persistWhenNil)
    var persistedWhenNilInt: Int!

    @Persisted(key: "notPersistedWhenNilInt", userDefaults: .standard, defaultValue: Int.random(in: 1...10))
    var notPersistedWhenNilInt: Int!
}

var foo = Foo()

UserDefaults.standard.object(forKey: "persistedWhenNilInt") // nil
foo.persistedWhenNilInt // 3
UserDefaults.standard.object(forKey: "persistedWhenNilInt") // 3
foo.persistedWhenNilInt // 3

UserDefaults.standard.object(forKey: "notPersistedWhenNilInt") // nil
foo.notPersistedWhenNilInt // 7
UserDefaults.standard.object(forKey: "notPersistedWhenNilInt") // nil
foo.notPersistedWhenNilInt // 7

// ...restart app

UserDefaults.standard.object(forKey: "persistedWhenNilInt") // 3
foo.persistedWhenNilInt // 3

UserDefaults.standard.object(forKey: "notPersistedWhenNilInt") // nil
foo.notPersistedWhenNilInt // 4

Property Wrapper Initialisation

To support dependency injection or to initialise more complex Persisted instances you may initialise the property wrapper in your own init functions:

class Foo {
    @Persisted
    var bar: String?

    init(userDefaults: UserDefaults) {
        _bar = Persisted(key: "foo-bar", userDefaults: userDefaults)
    }
}

Installation

Persist can be installed via SwiftPM by adding the package to the dependencies section and as the dependency of a target:

let package = Package(
    ...
    dependencies: [
        .package(url: "https://github.com/JosephDuffy/Persist.git", from: "1.0.0"),
    ],
    targets: [
        .target(name: "MyApp", dependencies: ["Persist"]),
    ],
    ...
)

License

The project is released under the MIT license. View the LICENSE file for the full license.

persist's People

Contributors

actions-user avatar dependabot[bot] avatar josephduffy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

persist's Issues

UserDefaults keys with a dot (`.`) will not call update listeners

Internally UserDefaults changes are monitored using KVO. When adding an observer via KVO the dot is used to access values (e.g. array.count would observer the count property of array).

The only way to work around this is to check the key in storeValue(_:key:) and call the update listeners if it contains a ., although this means that the observers will not be notified of changes directly applied to the UserDefaults.

[Security] Workflow tests.yml is using vulnerable action actions/checkout

The workflow tests.yml is referencing action actions/checkout using references v1. However this reference is missing the commit a6747255bd19d7a757dbdda8c654a9f84db19839 which may contain fix to the some vulnerability.
The vulnerability fix that is missing by actions version could be related to:
(1) CVE fix
(2) upgrade of vulnerable dependency
(3) fix to secret leak and others.
Please consider to update the reference to the action.

Syncing UserDefaults and NSUbiquitousKeyValueStore

Excellent work! Might I suggest support for UserDefaults & NSUbiquitousKeyValueStore synchronization? So, the user just needs to persist UserDefaults with a sync flag (or something similar), and all key/values in UserDefaults will be saved to NSUbiquitousKeyValueStore, or vice versa, depending on a Date value which determines which is more recent.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.