diff --git a/Sources/Defaults/Defaults+Extensions.swift b/Sources/Defaults/Defaults+Extensions.swift index 0e616d6..27c8322 100644 --- a/Sources/Defaults/Defaults+Extensions.swift +++ b/Sources/Defaults/Defaults+Extensions.swift @@ -90,14 +90,21 @@ extension Defaults.Serializable where Self: Codable & NSSecureCoding { public static var bridge: Defaults.CodableNSSecureCodingBridge { Defaults.CodableNSSecureCodingBridge() } } -extension Defaults.Serializable where Self: RawRepresentable { - public static var bridge: Defaults.RawRepresentableBridge { Defaults.RawRepresentableBridge() } +extension Defaults.Serializable where Self: Codable & NSSecureCoding & Defaults.PreferNSSecureCoding { + public static var bridge: Defaults.NSSecureCodingBridge { Defaults.NSSecureCodingBridge() } } -extension Defaults.Serializable where Self: RawRepresentable & Codable { +extension Defaults.Serializable where Self: Codable & RawRepresentable { public static var bridge: Defaults.RawRepresentableCodableBridge { Defaults.RawRepresentableCodableBridge() } } +extension Defaults.Serializable where Self: Codable & RawRepresentable & Defaults.PreferRawRepresentable { + public static var bridge: Defaults.RawRepresentableBridge { Defaults.RawRepresentableBridge() } +} + +extension Defaults.Serializable where Self: RawRepresentable { + public static var bridge: Defaults.RawRepresentableBridge { Defaults.RawRepresentableBridge() } +} extension Defaults.Serializable where Self: NSSecureCoding { public static var bridge: Defaults.NSSecureCodingBridge { Defaults.NSSecureCodingBridge() } } diff --git a/Sources/Defaults/Defaults+Protocol.swift b/Sources/Defaults/Defaults+Protocol.swift index 8d2f944..c767df9 100644 --- a/Sources/Defaults/Defaults+Protocol.swift +++ b/Sources/Defaults/Defaults+Protocol.swift @@ -100,3 +100,21 @@ public protocol DefaultsSetAlgebraSerializable: SetAlgebra, Defaults.Serializabl /// Convenience protocol for `Codable`. public protocol DefaultsCodableBridge: Defaults.Bridge where Serializable == String, Value: Codable {} + +/** +Ambiguous bridge selector protocol. This lets you select your preferred bridge when there are multiple possibilities. + +For example: + +``` +enum Interval: Int, Codable, Defaults.Serializable, Defaults.PreferRawRepresentable { + case tenMinutes = 10 + case halfHour = 30 + case oneHour = 60 +} +``` + +By default, if an `enum` conforms to `Codable` and `Defaults.Serializable`, it will use the `CodableBridge`, but by conforming to `Defaults.PreferRawRepresentable`, we can switch the bridge back to `RawRepresentableBridge`. +*/ +public protocol DefaultsPreferRawRepresentable: RawRepresentable {} +public protocol DefaultsPreferNSSecureCoding: NSSecureCoding {} diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index fde900e..d19f19a 100644 --- a/Sources/Defaults/Defaults.swift +++ b/Sources/Defaults/Defaults.swift @@ -19,6 +19,8 @@ public enum Defaults { public typealias Serializable = DefaultsSerializable public typealias CollectionSerializable = DefaultsCollectionSerializable public typealias SetAlgebraSerializable = DefaultsSetAlgebraSerializable + public typealias PreferRawRepresentable = DefaultsPreferRawRepresentable + public typealias PreferNSSecureCoding = DefaultsPreferNSSecureCoding public typealias Bridge = DefaultsBridge typealias CodableBridge = DefaultsCodableBridge diff --git a/Tests/DefaultsTests/DefaultsCodableEnumTests.swift b/Tests/DefaultsTests/DefaultsCodableEnumTests.swift index 18cf3f1..70f07d7 100644 --- a/Tests/DefaultsTests/DefaultsCodableEnumTests.swift +++ b/Tests/DefaultsTests/DefaultsCodableEnumTests.swift @@ -8,6 +8,12 @@ private enum FixtureCodableEnum: String, Defaults.Serializable & Codable & Hasha case oneHour = "1 Hour" } +private enum FixtureCodableEnumPreferRawRepresentable: Int, Hashable, Codable, Defaults.Serializable, Defaults.PreferRawRepresentable { + case tenMinutes = 10 + case halfHour = 30 + case oneHour = 60 +} + extension Defaults.Keys { fileprivate static let codableEnum = Key("codable_enum", default: .oneHour) fileprivate static let codableEnumArray = Key<[FixtureCodableEnum]>("codable_enum", default: [.oneHour]) @@ -118,6 +124,13 @@ final class DefaultsCodableEnumTests: XCTestCase { XCTAssertEqual(Defaults[.codableEnumDictionary]["1"], .halfHour) } + func testFixtureCodableEnumPreferRawRepresentable() { + let fixture: FixtureCodableEnumPreferRawRepresentable = .tenMinutes + let keyName = "testFixtureCodableEnumPreferRawRepresentable" + _ = Defaults.Key(keyName, default: fixture) + XCTAssertNotNil(UserDefaults.standard.integer(forKey: keyName)) + } + @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("observeCodableEnumKeyCombine", default: .tenMinutes) diff --git a/Tests/DefaultsTests/DefaultsCodableTests.swift b/Tests/DefaultsTests/DefaultsCodableTests.swift index c6f1ecb..9200663 100644 --- a/Tests/DefaultsTests/DefaultsCodableTests.swift +++ b/Tests/DefaultsTests/DefaultsCodableTests.swift @@ -21,6 +21,19 @@ private final class UnicornCodableAndNSSecureCoding: NSObject, NSSecureCoding, C } } +@objc(UnicornCodableAndPreferNSSecureCoding) +private final class UnicornCodableAndPreferNSSecureCoding: NSObject, NSSecureCoding, Codable, Defaults.Serializable, Defaults.PreferNSSecureCoding { + static let supportsSecureCoding = true + + func encode(with coder: NSCoder) {} + + init?(coder: NSCoder) {} + + override init() { + super.init() + } +} + extension Defaults.Keys { fileprivate static let codable = Key("codable", default: fixtureCodable) fileprivate static let codableArray = Key<[Unicorn]>("codable", default: [fixtureCodable]) @@ -135,7 +148,18 @@ final class DefaultsCodableTests: XCTestCase { func testCodableAndNSSecureCoding() { let fixture = UnicornCodableAndNSSecureCoding() - _ = Defaults.Key("testCodableAndNSSecureCoding", default: fixture) + let keyName = "testCodableAndNSSecureCoding" + _ = Defaults.Key(keyName, default: fixture) + XCTAssertNil(UserDefaults.standard.data(forKey: keyName)) + XCTAssertNotNil(UserDefaults.standard.string(forKey: keyName)) + } + + func testCodableAndPreferNSSecureCoding() { + let fixture = UnicornCodableAndPreferNSSecureCoding() + let keyName = "testCodableAndPreferNSSecureCoding" + _ = Defaults.Key(keyName, default: fixture) + XCTAssertNil(UserDefaults.standard.string(forKey: keyName)) + XCTAssertNotNil(UserDefaults.standard.data(forKey: keyName)) } @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, *) diff --git a/readme.md b/readme.md index 0cce272..453a78a 100644 --- a/readme.md +++ b/readme.md @@ -789,6 +789,27 @@ if let mimeType: mime = Defaults[.magic]["enum"]?.get() { For more examples, see [Tests/DefaultsAnySerializableTests](./Tests/DefaultsTests/DefaultsAnySeriliazableTests.swift). +### Serialization for ambiguous `Codable` type + +You may have a type that conforms to `Codable & NSSecureCoding` or a `Codable & RawRepresentable` enum. By default, `Defaults` will prefer the `Codable` conformance and use the `CodableBridge` to serialize it into a JSON string. If you want to serialize it as a `NSSecureCoding` data or use the raw value of the `RawRepresentable` enum, you can conform to `Defaults.PreferNSSecureCoding` or `Defaults.PreferRawRepresentable` to override the default bridge: + +```swift +enum mime: String, Codable, Defaults.Serializable, Defaults.PreferRawRepresentable { + case JSON = "application/json" +} + +extension Defaults.Keys { + static let magic = Key<[String: Defaults.AnySerializable]>("magic", default: [:]) +} + +print(UserDefaults.standard.string(forKey: "magic")) +//=> application/json +``` + +Had we not added `Defaults.PreferRawRepresentable`, the stored representation would have been `"application/json"` instead of `application/json`. + +This can also be useful if you conform a type you don't control to `Defaults.Serializable` as the type could receive `Codable` conformance at any time and then the stored representation would change, which could make the value unreadable. By explicitly defining which bridge to use, you ensure the stored representation will always stay the same. + ### Custom `Collection` type 1. Create your `Collection` and make its elements conform to `Defaults.Serializable`.