josephduffy/persist
`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.
```swift
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:
```swift
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:
```swift
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:
```swift
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:
```swift
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.
```swift
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.
```swift
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.
```swift
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.
```swift
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:
```swift
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.
Package Metadata
Repository: josephduffy/persist
Default branch: master
README: README.md