Add `Defaults.AnySerializable` (#73)

This commit is contained in:
hank121314 2021-07-12 20:10:43 +08:00 committed by GitHub
parent 6e42795824
commit 1b629298f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 781 additions and 28 deletions

View File

@ -0,0 +1,205 @@
import CoreGraphics
import Foundation
extension Defaults {
/**
Type-erased wrappers for `Defaults.Serializable` values.
It can be used when the user wants to create an `Any` value that conforms to `Defaults.Serializable`.
It will have an internal property `value` which` should always be a UserDefaults natively supported type.
`get` will deserialize internal value to the type that user explicit in the function parameter.
```
let any = Defaults.Key<Defaults.AnySerializable>("independentAnyKey", default: 121_314)
print(Defaults[any].get(Int.self)) //=> 121_314
```
- Note: the only way to assign a non-serializable value is using `ExpressibleByArrayLiteral` or `ExpressibleByDictionaryLiteral` to assign a type that is not UserDefaults natively supported type.
```
private enum mime: String, Defaults.Serializable {
case JSON = "application/json"
}
// Failed: Attempt to insert non-property list object
let any = Defaults.Key<Defaults.AnySerializable>("independentAnyKey", default: [mime.JSON])
```
*/
public struct AnySerializable: Defaults.Serializable {
var value: Any
public static let bridge = AnyBridge()
init<T>(value: T?) {
self.value = value ?? ()
}
public init<Value: Serializable>(_ value: Value) {
self.value = Value.toSerializable(value) ?? ()
}
public func get<Value: Serializable>() -> Value? { Value.toValue(value) }
public func get<Value: Serializable>(_: Value.Type) -> Value? { Value.toValue(value) }
public mutating func set<Value: Serializable>(_ newValue: Value) {
value = Value.toSerializable(newValue) ?? ()
}
public mutating func set<Value: Serializable>(_ newValue: Value, type: Value.Type) {
value = Value.toSerializable(newValue) ?? ()
}
}
}
extension Defaults.AnySerializable: Hashable {
public func hash(into hasher: inout Hasher) {
switch self.value {
case let value as Data:
return hasher.combine(value)
case let value as Date:
return hasher.combine(value)
case let value as Bool:
return hasher.combine(value)
case let value as UInt8:
return hasher.combine(value)
case let value as Int8:
return hasher.combine(value)
case let value as UInt16:
return hasher.combine(value)
case let value as Int16:
return hasher.combine(value)
case let value as UInt32:
return hasher.combine(value)
case let value as Int32:
return hasher.combine(value)
case let value as UInt64:
return hasher.combine(value)
case let value as Int64:
return hasher.combine(value)
case let value as UInt:
return hasher.combine(value)
case let value as Int:
return hasher.combine(value)
case let value as Float:
return hasher.combine(value)
case let value as Double:
return hasher.combine(value)
case let value as CGFloat:
return hasher.combine(value)
case let value as String:
return hasher.combine(value)
case let value as [AnyHashable: AnyHashable]:
return hasher.combine(value)
case let value as [AnyHashable]:
return hasher.combine(value)
default:
break
}
}
}
extension Defaults.AnySerializable: Equatable {
public static func == (lhs: Defaults.AnySerializable, rhs: Defaults.AnySerializable) -> Bool {
switch (lhs.value, rhs.value) {
case let (lhs as Data, rhs as Data):
return lhs == rhs
case let (lhs as Date, rhs as Date):
return lhs == rhs
case let (lhs as Bool, rhs as Bool):
return lhs == rhs
case let (lhs as UInt8, rhs as UInt8):
return lhs == rhs
case let (lhs as Int8, rhs as Int8):
return lhs == rhs
case let (lhs as UInt16, rhs as UInt16):
return lhs == rhs
case let (lhs as Int16, rhs as Int16):
return lhs == rhs
case let (lhs as UInt32, rhs as UInt32):
return lhs == rhs
case let (lhs as Int32, rhs as Int32):
return lhs == rhs
case let (lhs as UInt64, rhs as UInt64):
return lhs == rhs
case let (lhs as Int64, rhs as Int64):
return lhs == rhs
case let (lhs as UInt, rhs as UInt):
return lhs == rhs
case let (lhs as Int, rhs as Int):
return lhs == rhs
case let (lhs as Float, rhs as Float):
return lhs == rhs
case let (lhs as Double, rhs as Double):
return lhs == rhs
case let (lhs as CGFloat, rhs as CGFloat):
return lhs == rhs
case let (lhs as String, rhs as String):
return lhs == rhs
case let (lhs as [AnyHashable: Any], rhs as [AnyHashable: Any]):
return lhs.toDictionary() == rhs.toDictionary()
case let (lhs as [Any], rhs as [Any]):
return lhs.toSequence() == rhs.toSequence()
default:
return false
}
}
}
extension Defaults.AnySerializable: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.init(value: value)
}
}
extension Defaults.AnySerializable: ExpressibleByNilLiteral {
public init(nilLiteral _: ()) {
self.init(value: nil as Any?)
}
}
extension Defaults.AnySerializable: ExpressibleByBooleanLiteral {
public init(booleanLiteral value: Bool) {
self.init(value: value)
}
}
extension Defaults.AnySerializable: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self.init(value: value)
}
}
extension Defaults.AnySerializable: ExpressibleByFloatLiteral {
public init(floatLiteral value: Double) {
self.init(value: value)
}
}
extension Defaults.AnySerializable: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: Any...) {
self.init(value: elements)
}
}
extension Defaults.AnySerializable: ExpressibleByDictionaryLiteral {
public init(dictionaryLiteral elements: (AnyHashable, Any)...) {
self.init(value: [AnyHashable: Any](uniqueKeysWithValues: elements))
}
}
extension Defaults.AnySerializable: _DefaultsOptionalType {
/// Since nil cannot assign to `Any`, we use `Void` instead of `nil`.
public var isNil: Bool { value is Void }
}
extension Sequence {
fileprivate func toSequence() -> [Defaults.AnySerializable] {
map { Defaults.AnySerializable(value: $0) }
}
}
extension Dictionary {
fileprivate func toDictionary() -> [AnyHashable: Defaults.AnySerializable] {
reduce(into: [AnyHashable: Defaults.AnySerializable]()) { memo, tuple in memo[tuple.key] = Defaults.AnySerializable(value: tuple.value) }
}
}

View File

@ -297,3 +297,18 @@ extension Defaults {
}
}
}
extension Defaults {
public struct AnyBridge: Defaults.Bridge {
public typealias Value = Defaults.AnySerializable
public typealias Serializable = Any
public func deserialize(_ object: Serializable?) -> Value? {
Value(value: object)
}
public func serialize(_ value: Value?) -> Serializable? {
value?.value
}
}
}

View File

@ -0,0 +1,471 @@
import Defaults
import Foundation
import XCTest
private enum mime: String, Defaults.Serializable {
case JSON = "application/json"
case STREAM = "application/octet-stream"
}
private struct CodableUnicorn: Defaults.Serializable, Codable {
let is_missing: Bool
}
private struct Unicorn: Defaults.Serializable, Hashable {
static let bridge = UnicornBridge()
let is_missing: Bool
}
private struct UnicornBridge: Defaults.Bridge {
typealias Value = Unicorn
typealias Serializable = Bool
func serialize(_ value: Value?) -> Serializable? {
value?.is_missing
}
func deserialize(_ object: Serializable?) -> Value? {
Value(is_missing: object!)
}
}
extension Defaults.Keys {
fileprivate static let magic = Key<[String: Defaults.AnySerializable]>("magic", default: [:])
fileprivate static let anyKey = Key<Defaults.AnySerializable>("anyKey", default: "🦄")
fileprivate static let anyArrayKey = Key<[Defaults.AnySerializable]>("anyArrayKey", default: ["No.1 🦄", "No.2 🦄"])
fileprivate static let anyDictionaryKey = Key<[String: Defaults.AnySerializable]>("anyDictionaryKey", default: ["unicorn": "🦄"])
}
final class DefaultsAnySerializableTests: XCTestCase {
override func setUp() {
super.setUp()
Defaults.removeAll()
}
override func tearDown() {
super.tearDown()
Defaults.removeAll()
}
func testReadMeExample() {
let any = Defaults.Key<Defaults.AnySerializable>("anyKey", default: Defaults.AnySerializable(mime.JSON))
if let mimeType: mime = Defaults[any].get() {
XCTAssertEqual(mimeType, mime.JSON)
}
Defaults[any].set(mime.STREAM)
if let mimeType: mime = Defaults[any].get() {
XCTAssertEqual(mimeType, mime.STREAM)
}
Defaults[any].set(mime.JSON)
if let mimeType: mime = Defaults[any].get() {
XCTAssertEqual(mimeType, mime.JSON)
}
Defaults[.magic]["unicorn"] = "🦄"
Defaults[.magic]["number"] = 3
Defaults[.magic]["boolean"] = true
Defaults[.magic]["enum"] = Defaults.AnySerializable(mime.JSON)
XCTAssertEqual(Defaults[.magic]["unicorn"], "🦄")
XCTAssertEqual(Defaults[.magic]["number"], 3)
if let bool: Bool = Defaults[.magic]["unicorn"]?.get() {
XCTAssertTrue(bool)
}
XCTAssertEqual(Defaults[.magic]["enum"]?.get(), mime.JSON)
Defaults[.magic]["enum"]?.set(mime.STREAM)
if let value: String = Defaults[.magic]["unicorn"]?.get() {
XCTAssertEqual(value, "🦄")
}
if let mimeType: mime = Defaults[.magic]["enum"]?.get() {
XCTAssertEqual(mimeType, mime.STREAM)
}
Defaults[any].set(mime.JSON)
if let mimeType: mime = Defaults[any].get() {
XCTAssertEqual(mime.JSON, mimeType)
}
Defaults[any].set(mime.STREAM)
if let mimeType: mime = Defaults[any].get() {
XCTAssertEqual(mime.STREAM, mimeType)
}
}
func testKey() {
// Test Int
let any = Defaults.Key<Defaults.AnySerializable>("independentAnyKey", default: 121_314)
XCTAssertEqual(Defaults[any], 121_314)
// Test Int8
let int8 = Int8.max
Defaults[any].set(int8)
XCTAssertEqual(Defaults[any].get(), int8)
// Test Int16
let int16 = Int16.max
Defaults[any].set(int16)
XCTAssertEqual(Defaults[any].get(), int16)
// Test Int32
let int32 = Int32.max
Defaults[any].set(int32)
XCTAssertEqual(Defaults[any].get(), int32)
// Test Int64
let int64 = Int64.max
Defaults[any].set(int64)
XCTAssertEqual(Defaults[any].get(), int64)
// Test UInt
let uint = UInt.max
Defaults[any].set(uint)
XCTAssertEqual(Defaults[any].get(), uint)
// Test UInt8
let uint8 = UInt8.max
Defaults[any].set(uint8)
XCTAssertEqual(Defaults[any].get(), uint8)
// Test UInt16
let uint16 = UInt16.max
Defaults[any].set(uint16)
XCTAssertEqual(Defaults[any].get(), uint16)
// Test UInt32
let uint32 = UInt32.max
Defaults[any].set(uint32)
XCTAssertEqual(Defaults[any].get(), uint32)
// Test UInt64
let uint64 = UInt64.max
Defaults[any].set(uint64)
XCTAssertEqual(Defaults[any].get(), uint64)
// Test Double
Defaults[any] = 12_131.4
XCTAssertEqual(Defaults[any], 12_131.4)
// Test Bool
Defaults[any] = true
XCTAssertTrue(Defaults[any].get(Bool.self)!)
// Test String
Defaults[any] = "121314"
XCTAssertEqual(Defaults[any], "121314")
// Test Float
Defaults[any].set(12_131.456, type: Float.self)
XCTAssertEqual(Defaults[any].get(Float.self), 12_131.456)
// Test Date
let date = Date()
Defaults[any].set(date)
XCTAssertEqual(Defaults[any].get(Date.self), date)
// Test Data
let data = "121314".data(using: .utf8)
Defaults[any].set(data)
XCTAssertEqual(Defaults[any].get(Data.self), data)
// Test Array
Defaults[any] = [1, 2, 3]
if let array: [Int] = Defaults[any].get() {
XCTAssertEqual(array[0], 1)
XCTAssertEqual(array[1], 2)
XCTAssertEqual(array[2], 3)
}
// Test Dictionary
Defaults[any] = ["unicorn": "🦄", "boolean": true, "number": 3]
if let dictionary = Defaults[any].get([String: Defaults.AnySerializable].self) {
XCTAssertEqual(dictionary["unicorn"], "🦄")
XCTAssertTrue(dictionary["boolean"]!.get(Bool.self)!)
XCTAssertEqual(dictionary["number"], 3)
}
// Test Set
Defaults[any].set(Set([1]))
XCTAssertEqual(Defaults[any].get(Set<Int>.self)?.first, 1)
// Test URL
Defaults[any].set(URL(string: "https://example.com")!)
XCTAssertEqual(Defaults[any].get()!, URL(string: "https://example.com")!)
#if os(macOS)
// Test NSColor
Defaults[any].set(NSColor(red: CGFloat(103) / CGFloat(0xFF), green: CGFloat(132) / CGFloat(0xFF), blue: CGFloat(255) / CGFloat(0xFF), alpha: 0.987))
XCTAssertEqual(Defaults[any].get(NSColor.self)?.alphaComponent, 0.987)
#else
// Test UIColor
Defaults[any].set(UIColor(red: CGFloat(103) / CGFloat(0xFF), green: CGFloat(132) / CGFloat(0xFF), blue: CGFloat(255) / CGFloat(0xFF), alpha: 0.654))
XCTAssertEqual(Defaults[any].get(UIColor.self)?.cgColor.alpha, 0.654)
#endif
// Test Codable type
Defaults[any].set(CodableUnicorn(is_missing: false))
XCTAssertFalse(Defaults[any].get(CodableUnicorn.self)!.is_missing)
// Test Custom type
Defaults[any].set(Unicorn(is_missing: true))
XCTAssertTrue(Defaults[any].get(Unicorn.self)!.is_missing)
// Test nil
Defaults[any] = nil
XCTAssertEqual(Defaults[any], 121_314)
}
func testOptionalKey() {
let key = Defaults.Key<Defaults.AnySerializable?>("independentOptionalAnyKey")
XCTAssertNil(Defaults[key])
Defaults[key] = 12_131.4
XCTAssertEqual(Defaults[key], 12_131.4)
Defaults[key]?.set(mime.JSON)
XCTAssertEqual(Defaults[key]?.get(mime.self), mime.JSON)
Defaults[key] = nil
XCTAssertNil(Defaults[key])
}
func testArrayKey() {
let key = Defaults.Key<[Defaults.AnySerializable]>("independentArrayAnyKey", default: [123, 456])
XCTAssertEqual(Defaults[key][0], 123)
XCTAssertEqual(Defaults[key][1], 456)
Defaults[key][0] = 12_131.4
XCTAssertEqual(Defaults[key][0], 12_131.4)
}
func testSetKey() {
let key = Defaults.Key<Set<Defaults.AnySerializable>>("independentArrayAnyKey", default: [123])
XCTAssertEqual(Defaults[key].first, 123)
Defaults[key].insert(12_131.4)
XCTAssertTrue(Defaults[key].contains(12_131.4))
let date = Defaults.AnySerializable(Date())
Defaults[key].insert(date)
XCTAssertTrue(Defaults[key].contains(date))
let data = Defaults.AnySerializable("Hello World!".data(using: .utf8))
Defaults[key].insert(data)
XCTAssertTrue(Defaults[key].contains(data))
let int = Defaults.AnySerializable(Int.max)
Defaults[key].insert(int)
XCTAssertTrue(Defaults[key].contains(int))
let int8 = Defaults.AnySerializable(Int8.max)
Defaults[key].insert(int8)
XCTAssertTrue(Defaults[key].contains(int8))
let int16 = Defaults.AnySerializable(Int16.max)
Defaults[key].insert(int16)
XCTAssertTrue(Defaults[key].contains(int16))
let int32 = Defaults.AnySerializable(Int32.max)
Defaults[key].insert(int32)
XCTAssertTrue(Defaults[key].contains(int32))
let int64 = Defaults.AnySerializable(Int64.max)
Defaults[key].insert(int64)
XCTAssertTrue(Defaults[key].contains(int64))
let uint = Defaults.AnySerializable(UInt.max)
Defaults[key].insert(uint)
XCTAssertTrue(Defaults[key].contains(uint))
let uint8 = Defaults.AnySerializable(UInt8.max)
Defaults[key].insert(uint8)
XCTAssertTrue(Defaults[key].contains(uint8))
let uint16 = Defaults.AnySerializable(UInt16.max)
Defaults[key].insert(uint16)
XCTAssertTrue(Defaults[key].contains(uint16))
let uint32 = Defaults.AnySerializable(UInt32.max)
Defaults[key].insert(uint32)
XCTAssertTrue(Defaults[key].contains(uint32))
let uint64 = Defaults.AnySerializable(UInt64.max)
Defaults[key].insert(uint64)
XCTAssertTrue(Defaults[key].contains(uint64))
let bool: Defaults.AnySerializable = false
Defaults[key].insert(bool)
XCTAssertTrue(Defaults[key].contains(bool))
let float = Defaults.AnySerializable(Float(1213.14))
Defaults[key].insert(float)
XCTAssertTrue(Defaults[key].contains(float))
let cgFloat = Defaults.AnySerializable(CGFloat(12_131.415))
Defaults[key].insert(cgFloat)
XCTAssertTrue(Defaults[key].contains(cgFloat))
let string = Defaults.AnySerializable("Hello World!")
Defaults[key].insert(string)
XCTAssertTrue(Defaults[key].contains(string))
let array: Defaults.AnySerializable = [1, 2, 3, 4]
Defaults[key].insert(array)
XCTAssertTrue(Defaults[key].contains(array))
let dictionary: Defaults.AnySerializable = ["Hello": "World!"]
Defaults[key].insert(dictionary)
XCTAssertTrue(Defaults[key].contains(dictionary))
let unicorn = Defaults.AnySerializable(Unicorn(is_missing: true))
Defaults[key].insert(unicorn)
XCTAssertTrue(Defaults[key].contains(unicorn))
}
func testArrayOptionalKey() {
let key = Defaults.Key<[Defaults.AnySerializable]?>("testArrayOptionalAnyKey")
XCTAssertNil(Defaults[key])
Defaults[key] = [123]
Defaults[key]?.append(456)
XCTAssertEqual(Defaults[key]![0], 123)
XCTAssertEqual(Defaults[key]![1], 456)
Defaults[key]![0] = 12_131.4
XCTAssertEqual(Defaults[key]![0], 12_131.4)
}
func testNestedArrayKey() {
let key = Defaults.Key<[[Defaults.AnySerializable]]>("testNestedArrayAnyKey", default: [[123]])
Defaults[key][0].append(456)
XCTAssertEqual(Defaults[key][0][0], 123)
XCTAssertEqual(Defaults[key][0][1], 456)
Defaults[key].append([12_131.4])
XCTAssertEqual(Defaults[key][1][0], 12_131.4)
}
func testDictionaryKey() {
let key = Defaults.Key<[String: Defaults.AnySerializable]>("independentDictionaryAnyKey", default: ["unicorn": ""])
XCTAssertEqual(Defaults[key]["unicorn"], "")
Defaults[key]["unicorn"] = "🦄"
XCTAssertEqual(Defaults[key]["unicorn"], "🦄")
Defaults[key]["number"] = 3
Defaults[key]["boolean"] = true
XCTAssertEqual(Defaults[key]["number"], 3)
if let bool: Bool = Defaults[.magic]["unicorn"]?.get() {
XCTAssertTrue(bool)
}
Defaults[key]["set"] = Defaults.AnySerializable(Set([1]))
XCTAssertEqual(Defaults[key]["set"]!.get(Set<Int>.self)!.first, 1)
Defaults[key]["nil"] = nil
XCTAssertNil(Defaults[key]["nil"])
}
func testDictionaryOptionalKey() {
let key = Defaults.Key<[String: Defaults.AnySerializable]?>("independentDictionaryOptionalAnyKey")
XCTAssertNil(Defaults[key])
Defaults[key] = ["unicorn": "🦄"]
XCTAssertEqual(Defaults[key]?["unicorn"], "🦄")
Defaults[key]?["number"] = 3
Defaults[key]?["boolean"] = true
XCTAssertEqual(Defaults[key]?["number"], 3)
XCTAssertEqual(Defaults[key]?["boolean"], true)
}
func testDictionaryArrayKey() {
let key = Defaults.Key<[String: [Defaults.AnySerializable]]>("independentDictionaryArrayAnyKey", default: ["number": [1]])
XCTAssertEqual(Defaults[key]["number"]?[0], 1)
Defaults[key]["number"]?.append(2)
Defaults[key]["unicorn"] = ["No.1 🦄"]
Defaults[key]["unicorn"]?.append("No.2 🦄")
Defaults[key]["unicorn"]?.append("No.3 🦄")
Defaults[key]["boolean"] = [true]
Defaults[key]["boolean"]?.append(false)
XCTAssertEqual(Defaults[key]["number"]?[1], 2)
XCTAssertEqual(Defaults[key]["unicorn"]?[0], "No.1 🦄")
XCTAssertEqual(Defaults[key]["unicorn"]?[1], "No.2 🦄")
XCTAssertEqual(Defaults[key]["unicorn"]?[2], "No.3 🦄")
XCTAssertTrue(Defaults[key]["boolean"]![0].get(Bool.self)!)
XCTAssertFalse(Defaults[key]["boolean"]![1].get(Bool.self)!)
}
func testType() {
XCTAssertEqual(Defaults[.anyKey], "🦄")
Defaults[.anyKey] = 123
XCTAssertEqual(Defaults[.anyKey], 123)
}
func testArrayType() {
XCTAssertEqual(Defaults[.anyArrayKey][0], "No.1 🦄")
XCTAssertEqual(Defaults[.anyArrayKey][1], "No.2 🦄")
Defaults[.anyArrayKey].append(123)
XCTAssertEqual(Defaults[.anyArrayKey][2], 123)
}
func testDictionaryType() {
XCTAssertEqual(Defaults[.anyDictionaryKey]["unicorn"], "🦄")
Defaults[.anyDictionaryKey]["number"] = 3
XCTAssertEqual(Defaults[.anyDictionaryKey]["number"], 3)
Defaults[.anyDictionaryKey]["boolean"] = true
XCTAssertTrue(Defaults[.anyDictionaryKey]["boolean"]!.get(Bool.self)!)
Defaults[.anyDictionaryKey]["array"] = [1, 2]
if let array = Defaults[.anyDictionaryKey]["array"]?.get([Int].self) {
XCTAssertEqual(array[0], 1)
XCTAssertEqual(array[1], 2)
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveKeyCombine() {
let key = Defaults.Key<Defaults.AnySerializable>("observeAnyKeyCombine", default: 123)
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults
.publisher(key, options: [])
.map { ($0.oldValue, $0.newValue) }
.collect(2)
let expectedValue: [(Defaults.AnySerializable, Defaults.AnySerializable)] = [(123, "🦄"), ("🦄", 123)]
let cancellable = publisher.sink { tuples in
for (index, expected) in expectedValue.enumerated() {
XCTAssertEqual(expected.0, tuples[index].0)
XCTAssertEqual(expected.1, tuples[index].1)
}
expect.fulfill()
}
Defaults[key] = "🦄"
Defaults.reset(key)
cancellable.cancel()
waitForExpectations(timeout: 10)
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *)
func testObserveOptionalKeyCombine() {
let key = Defaults.Key<Defaults.AnySerializable?>("observeAnyOptionalKeyCombine")
let expect = expectation(description: "Observation closure being called")
let publisher = Defaults
.publisher(key, options: [])
.map { ($0.oldValue, $0.newValue) }
.collect(3)
let expectedValue: [(Defaults.AnySerializable?, Defaults.AnySerializable?)] = [(nil, 123), (123, "🦄"), ("🦄", nil)]
let cancellable = publisher.sink { tuples in
for (index, expected) in expectedValue.enumerated() {
if tuples[index].0?.get(Int.self) != nil {
XCTAssertEqual(expected.0, tuples[index].0)
XCTAssertEqual(expected.1, tuples[index].1)
} else if tuples[index].0?.get(String.self) != nil {
XCTAssertEqual(expected.0, tuples[index].0)
XCTAssertNil(tuples[index].1)
} else {
XCTAssertNil(tuples[index].0)
XCTAssertEqual(expected.1, tuples[index].1)
}
}
expect.fulfill()
}
Defaults[key] = 123
Defaults[key] = "🦄"
Defaults.reset(key)
cancellable.cancel()
waitForExpectations(timeout: 10)
}
func testObserveKey() {
let key = Defaults.Key<Defaults.AnySerializable>("observeAnyKey", default: 123)
let expect = expectation(description: "Observation closure being called")
var observation: Defaults.Observation!
observation = Defaults.observe(key, options: []) { change in
XCTAssertEqual(change.oldValue, 123)
XCTAssertEqual(change.newValue, "🦄")
observation.invalidate()
expect.fulfill()
}
Defaults[key] = "🦄"
observation.invalidate()
waitForExpectations(timeout: 10)
}
func testObserveOptionalKey() {
let key = Defaults.Key<Defaults.AnySerializable?>("observeAnyOptionalKey")
let expect = expectation(description: "Observation closure being called")
var observation: Defaults.Observation!
observation = Defaults.observe(key, options: []) { change in
XCTAssertNil(change.oldValue)
XCTAssertEqual(change.newValue, "🦄")
observation.invalidate()
expect.fulfill()
}
Defaults[key] = "🦄"
observation.invalidate()
waitForExpectations(timeout: 10)
}
}

118
readme.md
View File

@ -426,6 +426,20 @@ It has two associated types `Value` and `Serializable`.
- `serialize`: Executed before storing to the `UserDefaults` .
- `deserialize`: Executed after retrieving its value from the `UserDefaults`.
#### `Defaults.AnySerializable`
```swift
Defaults.AnySerializable<Value: Defaults.Serializable>(_ value: Value)
```
Type: `class`
Type-erased wrappers for `Defaults.Serializable` values.
- `get<Value: Defaults.Serializable>() -> Value?`: Retrieve the value which type is `Value` from the UserDefaults.
- `get<Value: Defaults.Serializable>(_: Value.Type) -> Value?`: Specific the `Value` that you want to retrieve, this will be useful in some ambiguous cases.
- `set<Value: Defaults.Serializable>(_ newValue: Value)`: Set newValue into `Defaults.AnySerializable`.
#### `Defaults.reset(keys…)`
Type: `func`
@ -662,6 +676,82 @@ Defaults[.setUser].first?.name //=> "Hello"
Defaults[.dictionaryUser]["user"]?.name //=> "Hello"
```
### Dynamic value
There might be situations where you want to use `[String: Any]` directly.
But `Defaults` need its value to conform to `Defaults.Serializable`, so here is a class `Defaults.AnySerializable` to overcome this limitation.
`Defaults.AnySerializable` only available for `Value` which conforms to `Defaults.Serializable`.
Warn: The type erasure should only be used when there's no other way to handle it because it has much worse performance. It should only be used in wrapped types. For example, wrapped in `Array`, `Set` or `Dictionary`.
#### Primitive type
`Defaults.AnySerializable` conforms to `ExpressibleByStringLiteral`, `ExpressibleByIntegerLiteral`, `ExpressibleByFloatLiteral`, `ExpressibleByBooleanLiteral`, `ExpressibleByNilLiteral`, `ExpressibleByArrayLiteral` and `ExpressibleByDictionaryLiteral`.
So it can assign directly with these primitive types.
```swift
let any = Defaults.Key<Defaults.AnySerializable>("anyKey", default: 1)
Defaults[any] = "🦄"
```
#### Other types
##### Using `get`, `set`
For other types, you will have to assign it like this.
```swift
enum mime: String, Defaults.Serializable {
case JSON = "application/json"
case STREAM = "application/octet-stream"
}
let any = Defaults.Key<Defaults.AnySerializable>("anyKey", default: [Defaults.AnySerializable(mime.JSON)])
if let mimeType: mime = Defaults[any].get() {
print(mimeType.rawValue) //=> "application/json"
}
Defaults[any].set(mime.STREAM)
if let mimeType: mime = Defaults[any].get() {
print(mimeType.rawValue) //=> "application/octet-stream"
}
```
#### Wrapped in `Array`, `Set`, `Dictionary`
`Defaults.AnySerializable` also support the above types wrapped in `Array`, `Set`, `Dictionary`
Here is the example for `[String: Defaults.AnySerializable]`.
```swift
extension Defaults.Keys {
static let magic = Key<[String: Defaults.AnySerializable]>("magic", default: [:])
}
enum mime: String, Defaults.Serializable {
case JSON = "application/json"
}
// …
Defaults[.magic]["unicorn"] = "🦄"
if let value: String = Defaults[.magic]["unicorn"]?.get() {
print(value)
//=> "🦄"
}
Defaults[.magic]["number"] = 3
Defaults[.magic]["boolean"] = true
Defaults[.magic]["enum"] = Defaults.AnySerializable(mime.JSON)
if let mimeType: mime = Defaults[.magic]["enum"]?.get() {
print(mimeType.rawValue)
//=> "application/json"
}
```
For more examples, see [Tests/DefaultsAnySerializableTests](./Tests/DefaultsTests/DefaultsAnySeriliazableTests.swift).
### Custom `Collection` type
1. Create your `Collection` and make its elements conform to `Defaults.Serializable`.
@ -797,34 +887,6 @@ Defaults[.stringSet].contains("World!") //=> true
After `Defaults` v5, you don't need to use `Codable` to store dictionary, `Defaults` supports storing dictionary natively.
For `Defaults` support types, see [Support types](#support-types).
There might be situations where you want to use `[String: Any]` directly.
Unfortunately, since `Any` can not conform to `Defaults.Serializable`, `Defaults` can not support it.
However, you can use the [`AnyCodable`](https://github.com/Flight-School/AnyCodable) package to work around this `Defaults.Serializable` limitation:
```swift
import AnyCodable
/// Important: Let AnyCodable conforms to Defaults.Serializable
extension AnyCodable: Defaults.Serializable {}
extension Defaults.Keys {
static let magic = Key<[String: AnyCodable]>("magic", default: [:])
}
// …
Defaults[.magic]["unicorn"] = "🦄"
if let value = Defaults[.magic]["unicorn"]?.value {
print(value)
//=> "🦄"
}
Defaults[.magic]["number"] = 3
Defaults[.magic]["boolean"] = true
```
### How is this different from [`SwiftyUserDefaults`](https://github.com/radex/SwiftyUserDefaults)?
It's inspired by that package and other solutions. The main difference is that this module doesn't hardcode the default values and comes with Codable support.