Define Compatible protocol that allows value to be plist compatible
The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
Next, define Defaults that accepts UserDefaults as initialize dependency, so that we can swap UserDefaults.
Key with generic type
We define Key with phantom type Value so we know which value this key is pointing to, this makes it easier to reason about the code.
Since Swift has limitation Static stored properties not supported in generic types, we can't extend our Key with static stored properties, we have to do via computed property
This works, but does not look nice. To workaround this, we define class AnyKey and make our Key class as well and inherited this AnyKey class.
Make a typealias typealias Keys = AnyKey so we can refer to Defaults.Keys when we define our keys.
public class Defaults {
public var suite: UserDefaults
public init(suite: UserDefaults = .standard) {
self.suite = suite
}
public subscript<Value: Compatible>(key: Key<Value>) -> Value {
get {
if let value = suite.object(forKey: key.name) as? Value {
return value
}
return key.defaultValue
}
set {
suite.set(newValue, forKey: key.name)
}
}
public func exists<Value: Compatible>(key: Key<Value>) -> Bool {
suite.object(forKey: key.name) != nil
}
}
extension Defaults {
public typealias Keys = AnyKey
public class Key<Value: Compatible>: AnyKey {
var defaultValue: Value
public init(_ name: String, default defaultValue: Value) {
self.defaultValue = defaultValue
super.init(name: name)
}
}
public class AnyKey {
var name: String
init(name: String) {
self.name = name
}
}
}
extension Defaults.AnyKey: Equatable {
public static func == (lhs: Defaults.AnyKey, rhs: Defaults.AnyKey) -> Bool {
lhs.name == rhs.name
}
}
extension Defaults.AnyKey: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
How about Optional
We can support Optional as well, as long as it's underlying value is compatible. Since the type is defined via Key, we can't accidentally use Optional when the Key has non Optional value
extension Optional: Compatible where Wrapped: Compatible {}
extension Defaults {
public subscript<Value: Compatible>(key: Key<Optional<Value>>) -> Value? {
get {
if let value = suite.object(forKey: key.name) as? Value {
return value
}
return nil
}
set {
if let newValue {
suite.set(newValue, forKey: key.name)
} else {
suite.removeObject(forKey: key.name)
}
}
}
}
extension Defaults.Keys {
static let optional = Defaults.Key<Int?>("optional.int", default: nil)
}
func testOptional() {
XCTAssertNil(defaults[.optional])
defaults[.optional] = 1
XCTAssertEqual(defaults[.optional], 1)
defaults[.optional] = nil
XCTAssertNil(defaults[.optional])
}
We want to have a swifty UserDefaults API that works with subscript and in a type safe manner.
UserDefaults plist compatibility
Define
Compatible
protocol that allows value to be plist compatibleNext, define
Defaults
that acceptsUserDefaults
as initialize dependency, so that we can swap UserDefaults.Key with generic type
We define
Key
with phantom typeValue
so we know which value this key is pointing to, this makes it easier to reason about the code.Since Swift has limitation
Static stored properties not supported in generic types
, we can't extend ourKey
with static stored properties, we have to do via computed propertyThis works, but does not look nice. To workaround this, we define class
AnyKey
and make ourKey
class as well and inherited thisAnyKey
class.Make a typealias
typealias Keys = AnyKey
so we can refer toDefaults.Keys
when we define our keys.How about Optional
We can support
Optional
as well, as long as it's underlying value is compatible. Since the type is defined viaKey
, we can't accidentally use Optional when the Key has non Optional value