Swiftpack.co - Package - JosephDuffy/Persist
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.

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 are 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.

Github

link
Stars: 32

Releases

v1.0.0 - 2020-09-02T09:50:45

Initial release with stable API 🎉

v1.0.0-rc10 - 2020-09-01T20:30:32

  • Fix update listeners not being called for UserDefaults keys with a dot (.)
    • A (suppressible) warning is printed when a key with a dot is used
  • Fix conversion of numbers stored in UserDefaults
    • Using NSNumber internally allows for values like Double(12) to be converted back to Doubles; previously UserDefaults would convert this to an Int
  • Improve API of StorableInUserDefaults and StorableInNSUbiquitousKeyValueStore
    • The protocol requirements are now empty (no more as* added to types)
    • Conforming a type to StorableInUserDefaults and StorableInNSUbiquitousKeyValueStore will now result in a crash
  • Allow read access while writing, e.g. inside an update listener

v1.0.0-rc9 - 2020-08-30T13:01:06

  • Add RawRepresentableTransformer
  • Fix crash when accessing Persisted.wrappedValue from an update listener
  • Fix warning when using Swift 5.3 (Xcode 12)

v1.0.0-rc8 - 2020-07-27T09:19:54

Expose Subscription

v1.0.0-rc7 - 2020-07-26T18:09:45

Mark Subscription internal and return AnyCancellable

This allows consumers to create Storage that is not tied to Subscription although it must still provide a type that conforms to Cancellable.

iOS deployment target has been raised to v9 to be ready for Xcode 12 release.

v1.0.0-rc6 - 2020-07-25T21:39:03

Improve property wrapper API and make default value lazy

Persisted now supports using the property’s assigned value as the default value (e.g. @Persisted(...) var foo = "bar" will default foo to "bar".

Default values not provided via the property's assigned value are now lazily evaluated.

v1.0.0-rc5 - 2020-07-20T13:38:39

Improve update API by removing double optionals

v1.0.0-rc4 - 2020-06-22T12:40:40

Improve API for Subscription/Cancellable

v1.0.0-rc3 - 2020-06-19T22:02:41

Add documentation

v1.0.0-rc2 - 2020-06-18T00:39:32

Much better handling of optional values, which also allows for non-optional values

v1.0.0-rc1 - 2020-05-30T23:42:18

Release candidate 1 for 1.0 stable release

Has lots of tests but little documentation. API has changes a lot since initial 0.1.0 but I’m happy with it now. Hopefully it is consistent and (once docs are added) easy to use.

v0.2.2 - 2020-05-26T22:25:10

Use jazzy for documenation

v0.2.1 - 2020-05-26T22:14:34

Improve release process

Tags starting with v will trigger a GitHub release and build and upload documentation (this is currently broken and will be fixed in v0.2.2). The release notes for the GitHub release are taken from the tag, which means they support markdown too :tada:

v0.2.1-rc6 - 2020-05-26T22:06:48

Debug Release

Tag contents. This is markdown.

v0.2.1-rc5 - 2020-05-26T21:59:18

Debug release notes

Commit contents

v0.2.1-rc4 - 2020-05-26T21:52:22

Debug description

v0.2.1-rc3 - 2020-05-26T21:49:56

Fix workflow syntax

Release v0.2.1-rc1 - 2020-05-26T21:36:41

Fix workflow syntax