1. 程式人生 > >Single-line persistence definitions in Swift

Single-line persistence definitions in Swift

A good key-value storage solution should have the following features:

  • Strongly-typed access
  • All possible keys in one location
  • A developer shouldn’t need to write setters and getters manually.
  • The location of the store should easily be changeable (e.g. you should be able to decide to to move a value from UserDefaults
    into the Keychain without too much additional effort)
  • References to the store should be markable as read-only

With the power of generics and ExpressibleByStringLiteral, we can create a solution that doesn’t rely on an enum existing somewhere with a list of keys or a manual list of accessors.

The common denominator of all key-value storage

All key-value storage solutions are capable of storing primitive objects. Let’s consider this to be a common denominator for future implementation, so all our stored objects should be capable of representing themselves as primitives.

protocol StoredObject {    associatedtype Primitive    func toPrimitive() -> Primitive?    static func from(primitive: Primitive) -> Self?}

If we want to store a primitive we can conform to this protocol very easily:

extension String: StoredObject {        typealias Primitive = String        func toPrimitive() -> Primitive? {        return self    }        static func from(primitive: Primitive) -> Primitive? {        return primitive    }}

Actually, you know what, we don’t want to repeat all that code for Int, Float, Bool etc. Let’s create another protocol to implement the two simple functions for us:

protocol StoredAsSelf: StoredObject where Primitive == Self { }extension StoredAsSelf {        func toPrimitive() -> Primitive? {        return self    }        static func from(primitive: Primitive) -> Primitive? {        return primitive    }}

So now for the primitive types we want to store all we need to do is:

extension String: StoredAsSelf {    typealias Primitive = String}extension Int: StoredAsSelf {    typealias Primitive = Int}extension Float: StoredAsSelf {    typealias Primitive = Float}

Great! If you want to store a non-primitive type (for example a Date), simply conform to the protocol by breaking it down into a primitive:

extension Date: StoredObject {    typealias Primitive = TimeInterval    func toPrimitive() -> Primitive? {        return timeIntervalSince1970    }    static func from(primitive: Primitive) -> Date? {        return Date(timeIntervalSince1970: primitive)    }}

It’s easy!

A generic storage wrapper

For each storage solution we want to use, we should make a strongly typed wrapper. I’ll be using UserDefaults as an example but you can of course implement this for any third-party solution.

ExpressibleByStringLiteral allows us to instantiate an object with a literal string. This is perfect for our needs as we need a key that our StoredObject gets paired with, and we can infer the type of StoredObject using generics. But if you’ve ever used ExpressibleByStringLiteral before you’ll know that there are three initializers you need to implement: init(stringLiteral:), init(unicodeScalarLiteral:) and init(extendedGraphemeClusterLiteral:). In my mind this is unnecessary, so (again) let’s create a protocol to sneakily implement the latter two:

protocol StringLiteralBoilerplate {    init(stringLiteral value: String)}extension StringLiteralBoilerplate {    typealias StringLiteralType = String    init(unicodeScalarLiteral value: String) {        self.init(stringLiteral: value)    }    init(extendedGraphemeClusterLiteral value: String) {        self.init(stringLiteral: value)    }}

Now all that’s left is to create our storage wrapper. It’s going to have a generic type that must be a StoredObject and it’s going to complete the implementation of ExpressibleByStringLiteral:

struct Storable<Object>: ExpressibleByStringLiteral, StringLiteralBoilerplate where Object: StoredObject {    private let key: String    private let store = UserDefaults.standard
    init(stringLiteral value: String) {        self.key = value    }    var value: Object? {        set {            store.set(newValue?.toPrimitive(), forKey: key)        }        get {            guard let p = store.value(forKey: key) as? Object.Primitive else { return nil }            return Object.from(primitive: p)        }    }}

Again, you don’t need to use UserDefaults here — if you also need Keychain storage, simply create another similar struct called KeychainStorable and fill it out appropriately!

Putting it all together

I’ve set up my persistence definitions in a few apps like this:

struct Persistence {        struct TabBar {        var infoiewDate: Storable<Date> = "t.viewDate.info"        var diaryViewDate: Storable<Date> = "t.viewDate.diary"        var messagesViewDate: Storable<Date> = "t.viewDate.mess"    }
    struct Utility {        var upgradeAlertIgnored: Storable<Bool> = "u.upgradeIgnored"        var token: KeychainStorable<String> = "u.token"        var graphData: RAMStorable<[Graph.Body]> = "u.graphData"    }
    ...}

Then, in your class that needs to read the stored data, instantiate the group of items you need:

private var persistence = Persistence.Utility()
...
persistence.upgradeAlertIgnored.value = true // set/get using .value

Also, a nice side-effect of using struct when defining groups is that if you use let in the above example your persistence reference will be read-only! And of course if one day you decide a specific value should for example be stored in the Keychain instead of in UserDefaults you can simply change the name of the storage struct.

Finally, as access to storage is practically singleton-like you shouldn’t have any trouble when writing Unit Tests — just create another instance of the persistence group and assert values as required.

I’ve been working in Tokyo for the past few months! I’ve added this photo I took because a) I like it and b) Medium won’t let me choose the first image as the featured image unless there’s a second one ?