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