diff --git a/Sources/Defaults/Defaults+AnySerializable.swift b/Sources/Defaults/Defaults+AnySerializable.swift new file mode 100644 index 0000000..addb667 --- /dev/null +++ b/Sources/Defaults/Defaults+AnySerializable.swift @@ -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("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("independentAnyKey", default: [mime.JSON]) + ``` + */ + public struct AnySerializable: Defaults.Serializable { + var value: Any + public static let bridge = AnyBridge() + + init(value: T?) { + self.value = value ?? () + } + + public init(_ value: Value) { + self.value = Value.toSerializable(value) ?? () + } + + public func get() -> Value? { Value.toValue(value) } + + public func get(_: Value.Type) -> Value? { Value.toValue(value) } + + public mutating func set(_ newValue: Value) { + value = Value.toSerializable(newValue) ?? () + } + + public mutating func set(_ 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) } + } +} diff --git a/Sources/Defaults/Defaults+Bridge.swift b/Sources/Defaults/Defaults+Bridge.swift index b41287a..c440f6e 100644 --- a/Sources/Defaults/Defaults+Bridge.swift +++ b/Sources/Defaults/Defaults+Bridge.swift @@ -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 + } + } +} diff --git a/Tests/DefaultsTests/DefaultsAnySeriliazableTests.swift b/Tests/DefaultsTests/DefaultsAnySeriliazableTests.swift new file mode 100644 index 0000000..7c29a39 --- /dev/null +++ b/Tests/DefaultsTests/DefaultsAnySeriliazableTests.swift @@ -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("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("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("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.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("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>("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.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("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("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("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("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) + } +} diff --git a/readme.md b/readme.md index f878f2c..b6aa877 100644 --- a/readme.md +++ b/readme.md @@ -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: Value) +``` + +Type: `class` + +Type-erased wrappers for `Defaults.Serializable` values. + +- `get() -> Value?`: Retrieve the value which type is `Value` from the UserDefaults. +- `get(_: Value.Type) -> Value?`: Specific the `Value` that you want to retrieve, this will be useful in some ambiguous cases. +- `set(_ 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("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("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.