{"id":13995388,"url":"https://github.com/JosephDuffy/Persist","last_synced_at":"2025-07-22T21:32:51.185Z","repository":{"id":37704828,"uuid":"252309278","full_name":"JosephDuffy/Persist","owner":"JosephDuffy","description":"Extensible typesafe storage utilising property wrappers. Supports transformers such as Codable. Built in support for UserDefaults, NSUbiquitousKeyValueStore, FileManager, and in memory storage.","archived":false,"fork":false,"pushed_at":"2024-10-11T10:36:07.000Z","size":3705,"stargazers_count":47,"open_issues_count":6,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-11-18T03:03:15.657Z","etag":null,"topics":["nsubiquitouskeyvaluestore","property-wrapper","swift","swiftpm","userdefaults"],"latest_commit_sha":null,"homepage":"https://josephduffy.github.io/Persist/","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JosephDuffy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"josephduffy"}},"created_at":"2020-04-01T23:29:16.000Z","updated_at":"2024-08-19T08:56:04.000Z","dependencies_parsed_at":"2024-01-20T17:57:20.017Z","dependency_job_id":"8f7d269b-6b1e-4d39-85a7-542c2c0f5787","html_url":"https://github.com/JosephDuffy/Persist","commit_stats":{"total_commits":296,"total_committers":5,"mean_commits":59.2,"dds":0.09459459459459463,"last_synced_commit":"e8bfd613d498704937a50d09bcd178dffb20cc4c"},"previous_names":[],"tags_count":34,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosephDuffy%2FPersist","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosephDuffy%2FPersist/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosephDuffy%2FPersist/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JosephDuffy%2FPersist/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JosephDuffy","download_url":"https://codeload.github.com/JosephDuffy/Persist/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":227177936,"owners_count":17743207,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nsubiquitouskeyvaluestore","property-wrapper","swift","swiftpm","userdefaults"],"created_at":"2024-08-09T14:03:22.731Z","updated_at":"2024-11-29T17:31:38.756Z","avatar_url":"https://github.com/JosephDuffy.png","language":"Swift","funding_links":["https://github.com/sponsors/josephduffy"],"categories":["Swift"],"sub_categories":[],"readme":"# Persist\n\n[![Tests](https://github.com/JosephDuffy/Persist/workflows/Tests/badge.svg)](https://github.com/JosephDuffy/Persist/actions?query=workflow%3ATests)\n![Supported Xcode Versions](https://img.shields.io/badge/Xcode-11.7%20%7C%2012.5.1%20%7C%2013.2.1-success)\u003c!---xcode-version-badge-markdown--\u003e\n[![codecov](https://codecov.io/gh/JosephDuffy/Persist/branch/master/graph/badge.svg)](https://codecov.io/gh/JosephDuffy/Persist)\n[![Documentation](https://josephduffy.github.io/Persist/badge.svg)](https://josephduffy.github.io/Persist/)\n[![SwiftPM Compatible](https://img.shields.io/badge/SwiftPM-compatible-4BC51D.svg?style=flat)](https://github.com/apple/swift-package-manager)\n\n`Persist` is a framework that aids with persisting and retrieving values, with support for transformations such as storing as JSON data.\n\n## Usage\n\nPersist provides the `Persister` class, which can be used to persist and retrieve values from various forms of storage.\n\nThe `Persisted` property wrapper wraps a `Persister`, making it easy to have a property that automatically persists its value.\n\n```swift\nclass Foo {\n    enum Bar: Int {\n        case firstBar = 1\n        case secondBar = 2\n    }\n\n    @Persisted(key: \"foo-bar\", userDefaults: .standard, transformer: RawRepresentableTransformer())\n    var bar: Bar = .firstBar\n\n    @Persisted(key: \"foo-baz\", userDefaults: .standard)\n    var baz: String?\n}\n\nlet foo = Foo()\n\nfoo.bar // \"Bar.firstBar\"\nfoo.bar = .secondBar\nUserDefaults.standard.object(forKey: \"foo-bar\") // 2\n\nfoo.baz // nil\nfoo.baz = \"new-value\"\nUserDefaults.standard.object(forKey: \"foo-baz\") // \"new-value\"\n```\n\n`Persist` includes out of the box support for:\n\n- `UserDefaults`\n- `NSUbiquitousKeyValueStore`\n- `FileManager`\n- `InMemoryStorage` (a simple wrapper around a dictionary)\n\n### Catching Errors\n\n`Persister`'s `persist(_:)` and `retrieveValueOrThrow()` functions will throw if the storage or transformer throws an error.\n\n`Persisted` wraps a `Persister` and exposes it as the `projectedValue`, which allows you to catch errors:\n\n```swift\nclass Foo {\n    @Persisted(key: \"foo-bar\", userDefaults: .standard)\n    var bar: String?\n}\n\ndo {\n    let foo = Foo()\n    try foo.$bar.persist(\"new-value\")\n    try foo.$bar.retrieveValueOrThrow()\n} catch {\n    // Something went wrong\n}\n```\n\n### Subscribing to Updates\n\nWhen targeting macOS 10.15, iOS 13, tvOS 13, or watchOS 6 or greater Combine can be used to subscribe to updates:\n\n```swift\nclass Foo {\n    @Persisted(key: \"foo-bar\", userDefaults: .standard)\n    var bar: String?\n}\n\nlet foo = Foo()\nlet subscription = foo.$bar.updatesPublisher.sink { result in\n    switch result {\n    case .success(let update):\n        print(\"New value:\", update.newValue)\n\n        switch update.event {\n        case .persisted(let newValue):\n            print(\"Value updated to:\", newValue)\n            // `update.newValue` will be new value\n        case .removed:\n            print(\"Value was deleted\")\n            // `update.newValue` will be default value\n        }\n    case .failure(let error):\n        print(\"Error occurred retrieving value after update:\", error)\n    }\n}\n```\n\nFor versions prior to macOS 10.15, iOS 13, tvOS 13, or watchOS 6 a closure API is provided:\n\n```swift\nclass Foo {\n    @Persisted(key: \"foo-bar\", userDefaults: .standard)\n    var bar: String?\n}\n\nlet foo = Foo()\nlet subscription = foo.$bar.addUpdateListener() { result in\n    switch result {\n    case .success(let update):\n        print(\"New value:\", update.newValue)\n\n        switch update.event {\n        case .persisted(let newValue):\n            print(\"Value updated to:\", newValue)\n            // `update.newValue` will be new value\n        case .removed:\n            print(\"Value was deleted\")\n            // `update.newValue` will be default value\n        }\n    case .failure(let error):\n        print(\"Error occurred retrieving value after update:\", error)\n    }\n}\n```\n\n### Transformers\n\nSome 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:\n\n```swift\nstruct Bar: Codable {\n    var baz: String\n}\n\nclass Foo {\n    @Persisted(key: \"bar\", userDefaults: .standard, transformer: JSONTransformer())\n    var bar: Bar?\n}\n\nlet foo = Foo()\nlet subscription = foo.$bar.addUpdateListener() { result in\n    switch result {\n    case .success(let update):\n        // `update.newValue` is a `Bar?`\n        print(\"New value:\", update.newValue)\n\n        switch update.event {\n        case .persisted(let bar):\n            // `bar` is the decoded `Bar`\n            print(\"Value updated to:\", bar)\n        case .removed:\n            print(\"Value was deleted\")\n        }\n    case .failure(let error):\n        print(\"Error occurred retrieving value after update:\", error)\n    }\n}\n```\n\nTransformers are typesafe, e.g. `JSONTransformer` is only usable when the value to be stored is `Codable` and the `Storage` supports `Data`.\n\n#### Chaining Transformers\n\nIf a value should go through multiple transformers you can chain them.\n\n```swift\nstruct Bar: Codable {\n    var baz: String\n}\n\npublic struct BarTransformer: Transformer {\n\n    public func transformValue(_ bar: Bar) -\u003e Bar {\n        var bar = bar\n        bar.baz = \"transformed\"\n        return bar\n    }\n\n    public func untransformValue(_ bar: Bar) -\u003e Bar {\n        return bar\n    }\n\n}\n\nclass Foo {\n    @Persisted(key: \"bar\", userDefaults: .standard, transformer: BarTransformer().append(JSONTransformer()))\n    var bar: Bar?\n}\n\nlet foo = Foo()\nlet bar = Bar(baz: \"example value\")\nfoo.bar = bar\nfoo.bar.baz // \"transformed\"\n```\n\n### Default Values\n\nA default value may be provided that will be used when the persister returns `nil` or throws and error.\n\n```swift\nstruct Foo {\n    @Persisted(key: \"bar\", userDefaults: .standard)\n    var bar = \"default\"\n}\n\nvar foo = Foo()\nfoo.bar // \"default\"\n```\n\nWhen provided as the `defaultValue` parameter the value is evaluated lazily when first required.\n\n```swift\nfunc makeUUID() -\u003e UUID {\n    print(\"Making UUID\")\n    return UUID()\n}\n\nstruct Foo {\n    @Persisted(key: \"bar\", userDefaults: .standard, defaultValue: makeUUID())\n    var bar: UUID\n}\n\n/**\n This would not print anything because the default value is never required.\n */\nvar foo = Foo()\nfoo.bar = UUID()\n\n/**\n This would print \"Making UUID\" once.\n */\nvar foo = Foo()\nlet firstCall = foo.bar\nlet secondCall = foo.bar\nfirstCall == secondCall // true\n```\n\nThe 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.\n\n```swift\nstruct Foo {\n    @Persisted(key: \"persistedWhenNilInt\", userDefaults: .standard, defaultValue: Int.random(in: 1...10), defaultValuePersistBehaviour: .persistWhenNil)\n    var persistedWhenNilInt: Int!\n\n    @Persisted(key: \"notPersistedWhenNilInt\", userDefaults: .standard, defaultValue: Int.random(in: 1...10))\n    var notPersistedWhenNilInt: Int!\n}\n\nvar foo = Foo()\n\nUserDefaults.standard.object(forKey: \"persistedWhenNilInt\") // nil\nfoo.persistedWhenNilInt // 3\nUserDefaults.standard.object(forKey: \"persistedWhenNilInt\") // 3\nfoo.persistedWhenNilInt // 3\n\nUserDefaults.standard.object(forKey: \"notPersistedWhenNilInt\") // nil\nfoo.notPersistedWhenNilInt // 7\nUserDefaults.standard.object(forKey: \"notPersistedWhenNilInt\") // nil\nfoo.notPersistedWhenNilInt // 7\n\n// ...restart app\n\nUserDefaults.standard.object(forKey: \"persistedWhenNilInt\") // 3\nfoo.persistedWhenNilInt // 3\n\nUserDefaults.standard.object(forKey: \"notPersistedWhenNilInt\") // nil\nfoo.notPersistedWhenNilInt // 4\n```\n\n### Property Wrapper Initialisation\n\nTo support dependency injection or to initialise more complex `Persisted` instances you may initialise the property wrapper in your own init functions:\n\n```swift\nclass Foo {\n    @Persisted\n    var bar: String?\n\n    init(userDefaults: UserDefaults) {\n        _bar = Persisted(key: \"foo-bar\", userDefaults: userDefaults)\n    }\n}\n```\n\n## Installation\n\nPersist can be installed via [SwiftPM](https://github.com/apple/swift-package-manager) by adding the package to the dependencies section and as the dependency of a target:\n\n```swift\nlet package = Package(\n    ...\n    dependencies: [\n        .package(url: \"https://github.com/JosephDuffy/Persist.git\", from: \"1.0.0\"),\n    ],\n    targets: [\n        .target(name: \"MyApp\", dependencies: [\"Persist\"]),\n    ],\n    ...\n)\n```\n\n# License\n\nThe project is released under the MIT license. View the [LICENSE](./LICENSE) file for the full license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FJosephDuffy%2FPersist","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FJosephDuffy%2FPersist","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FJosephDuffy%2FPersist/lists"}