Issue #972
We want to have a swifty UserDefaults API that works with subscript and in a type safe manner.
extension Defaults.Keys {
static let string = Defaults.Key("string", default: "0")
}
XCTAssertEqual(defaults[.string], "0")
defaults[.string] = "1"
XCTAssertEqual(defaults[.string], "1")
UserDefaults plist compatibility
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.
public protocol Compatible: Equatable {}
extension Int: Compatible {}
extension String: Compatible {}
extension Bool: Compatible {}
extension Date: Compatible {}
extension Array: Compatible where Element: Compatible {}
extension Dictionary: Compatible where Key: Compatible, Value: Compatible {}
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
extension Defaults.Key {
static var string: Defaults.Key<String> { .init("string", default: "0") }
}
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])
}