Git Product home page Git Product logo

persistedpropertywrapper's Introduction

Persisted Property Wrapper

Swift Swift Version Cocoapods platforms GitHub

Persisted Property Wrapper is a Swift library to enable extremely easy persistance of variables in the UserDefaults database on Apple platforms.

To use Persisted Property Wrapper you simply annotate a variable as being @Persisted. It supports the standard UserDefaults types (Int, String, Bool, Date and more), along with RawRepresentable enums where the RawValue is storable in UserDefaults, as well as any type which conforms to Codable or NSSecureCoding. Plus of course any Optional wrapper of any of these types. The type-validity is checked at compile-time: attempting to use on any variables of a non-supported type will cause a compile-time error.

Usage

Stick a @Persisted attribute on your variable.

The first argument of the initializer is the string key under which the value will be stored in UserDefaults. If the type is non-Optional, you must also supply a defaultValue, which will be used when there is no value stored in UserDefaults.

For example:

@Persisted("UserSetting1", defaultValue: 42)
var someUserSetting: Int

@Persisted("UserSetting2") // defaultValue not necessary since Int? is an Optional type
var someOtherUserSetting: Int?

Storing Enums

Want to store an enum value? If the enum has a backing type which is supported for storage in UserDefaults, then those can also be marked as @Persisted, and the actual value stored in UserDefaults will be the enum's raw value. For example:

enum AppTheme: Int {
    case brightRed
    case vibrantOrange
    case plainBlue
}

struct ThemeSettings {
    // Stores the underlying integer backing the selected AppTheme
    @Persisted("theme", defaultValue: .plainBlue)
    var selectedTheme: AppTheme
}

Storing Codable types

Any codable type can be Persisted too; this will store in UserDefaults the JSON-encoded representation of the variable. For example:

struct AppSettings: Codable {
    var welcomeMessage = "Hello world!"
    var isSpecialModeEnabled = false
    var launchCount = 0

    @Persisted(encodedDataKey: "appSettings", defaultValue: .init())
    static var current: AppSettings
}

// Example usage: this will update the value of the stored AppSettings
func appDidLaunch() {
    AppSettings.current.launchCount += 1
}

Note that the argument label encodedDataKey must be used. This is required to remove ambiguity about which storage method is used, since UserDefaults-storable types can be Codable too.

For example, the following two variables are stored via different mechanisms:

// Stores the integer in UserDefaults
@Persisted("storedAsInteger", defaultValue: 10)
var storedAsInteger: Int

// Store the data of a JSON-encoded representation of the value. Don't use on iOS 12!
@Persisted(encodedDataKey: "storedAsData", defaultValue: 10)
var storedAsData: Int

Note: on iOS 12, using the encodedDataKey initializer with a value which would encode to a JSON fragment (e.g. Int, String, Bool, etc) will cause a crash. This is due to a bug in the Swift runtime shipped prior to iOS 13. Using encodedDataKey has no benefit in these cases anyway.

Storing types which implement NSCoding

Any NSObject which conforms to NSSecureCoding can be Persisted too; this will store in UserDefaults the encoded representation of the object obtained from NSKeyedArchiver. For example:

class CloudKitSyncManager {
    @Persisted(archivedDataKey: "ckServerChangeToken")
    var changeToken: CKServerChangeToken?
}

Note that the argument label archivedDataKey must be used. As above, this is required to remove ambiguity about which storage method is used.

Note: this storage mechanism is only supported on iOS 11 and up.

Alternative Storage

By default, a @Persisted property is stored in the UserDefaults.standard database; to store values in a different location, pass the storage: parameter to the property wrapper:

extension UserDefaults {
    static var alternative = UserDefaults(suiteName: "alternative")!
}

@Persisted("alternativeStoredValue", storage: .alternative)
var alternativeStoredValue: Int?

Why a Library?

After all, there are lots of examples of similar utilities on the web. For example, this post by John Sundell shows how a @UserDefaultsBacked property wrapper can be written in a handful of lines.

However, during development of my app, I found that I really wanted to store enum values in UserDefaults. For any enum which is backed by integer or a string, there was an obvious ideal implementation - store the enum's raw value. To provide a single API to persist both UserDefaults-supported types as well as enum values backed by UserDefaults-supported types proved a little tricky; adding the requirement that everything needed to also work on Optional wrappers of any supported type, and the problem became more complex still. Once solved for my app, I thought why not package up?

Requirements

  • Xcode 12
  • Swift 5.3

Installation

Swift Package Manager

Add https://github.com/AndrewBennet/PersistedPropertyWrapper.git as a Swift Package Dependency in Xcode.

CocoaPods

To install via CocoaPods, add the following line to your Podfile:

pod 'PersistedPropertyWrapper', '~> 2.0'

Manually

Copy the contents of the Sources directory into your project.

Alternatives

  • SwiftyUserDefaults has more functionality, but you are required to define your stored properties in a specific extension.
  • AppStorage: native Apple property wrapper, but tailored to (and defined in) SwiftUI, and only available in iOS 14

persistedpropertywrapper's People

Contributors

andrewbennet avatar rob-secondstage 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

Watchers

 avatar  avatar  avatar

persistedpropertywrapper's Issues

Redundant conformance constraint 'Convertor': 'PersistedStorageConvertor' warnings

This project is great! I've been working on something similar for my app but was having trouble with the optionals. Your approach helped me a great deal!

The current code gives Redundant conformance constraint 'Convertor': 'PersistedStorageConvertor' warnings for all of the initializer extensions. In Swift 5.3, you could use Contextual Where Clauses to fix this, and as a side benefit, you only need one extension.

Such as:

extension Persisted {
    init(_ key: String, defaultValue: Exposed, storage: UserDefaults = .standard) where Convertor == IdentityStorageConvertor<NonOptionalExposed>, Exposed == NonOptionalExposed {
        self.init(key: key, defaultValue: defaultValue, valueConvertor: .init(), storage: storage)
    }
    init(_ key: String, storage: UserDefaults = .standard) where Convertor == IdentityStorageConvertor<NonOptionalExposed>, Exposed == NonOptionalExposed? {
        self.init(key: key, defaultValue: nil, valueConvertor: .init(), storage: storage)
    }
    init(_ key: String, defaultValue: Exposed, storage: UserDefaults = .standard)  where Convertor == RawRepresentableStorageConvertor<NonOptionalExposed>, NonOptionalExposed: RawRepresentable, Exposed == NonOptionalExposed {
        self.init(key: key, defaultValue: defaultValue, valueConvertor: RawRepresentableStorageConvertor(), storage: storage)
    }
    init(_ key: String, storage: UserDefaults = .standard) where Convertor == RawRepresentableStorageConvertor<NonOptionalExposed>, NonOptionalExposed: RawRepresentable, Exposed == NonOptionalExposed? {
        self.init(key: key, defaultValue: nil, valueConvertor: RawRepresentableStorageConvertor(), storage: storage)
    }
    // Note the different parameter name in the following: encodedDataKey vs unnamed. This is reqired since some Codable types
    // are also UserDefaultsPrimitive or RawRepresentable. We need a different key to be able to avoid ambiguity.
    init(encodedDataKey key: String, defaultValue: Exposed, storage: UserDefaults = .standard) where Convertor == CodableStorageConvertor<NonOptionalExposed>, Exposed == NonOptionalExposed {
        self.init(key: key, defaultValue: defaultValue, valueConvertor: CodableStorageConvertor(), storage: storage)
    }
    init(encodedDataKey key: String, storage: UserDefaults = .standard) where Convertor == CodableStorageConvertor<NonOptionalExposed>, Exposed == NonOptionalExposed? {
        self.init(key: key, defaultValue: nil, valueConvertor: CodableStorageConvertor(), storage: storage)
    }
}

Crash on model scheme change

If you change type definition of Codable instance, then you get crash immediately by force try.
Maybe Codable is not fully supported?

Add 'projectedValue' to allow access to 'key' and 'defaultValue'

First, thanks for letting me contribute (in a small way) to your project. This is the first pull request I've ever done and the first time I've contributed anything to open source!

A suggested feature: add projectedValue to the property wrapper to allow easy access to the key and defaultValue properties. This can be useful when needing to specify these values later, for example, when using @AppStorage in SwiftUI.

Here is an example based on how I'm using this:

In the property wrapper:

    public var projectedValue: Self {
        return self
    }

Then, I use a struct to have easy access to all my persisted values:

struct Settings {
    @Persisted(key: "isFirstRun", defaultValue: true)
    static var isFirstRun

    @Persisted(key: "plantNameOrder", defaultValue: Plant.DisplayNameOrder.botanicalNameFirst)
    static var plantNameOrder: Plant.DisplayNameOrder
}

While that makes it easy to access settings throughout the app: Settings.plantNameOrder, it also, along with projectedValue makes using this in SwiftUI quite nice:

struct ContentView: View {
    @AppStorage(Settings.$plantNameOrder.key) var plantNameOrder = Settings.$plantNameOrder.defaultValue
    // ... rest of implementation 
}

Now, instead of having to make sure I get the string for key correct, and use the same default value, I can use what I've already done.

I just thought I'd suggest this based on my own usage. If you don't think this fits here, I totally get it. If you do, I'm happy to do another pull request and add something to the readme that shows this usage.

Thanks again.

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.