Last active
October 20, 2023 23:58
-
-
Save nevillco/6a15b5829cbd104f67affc0dbf7fc2d9 to your computer and use it in GitHub Desktop.
Codable and Custom Keyed Dictionaries
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Code from: https://www.connorneville.com/blog/swift-codable-and-custom-keyed-dictionaries | |
| // Tested against Xcode 15.0 Playground | |
| import Foundation | |
| // MARK: Starting with Codable | |
| struct SocialGroup: Codable { | |
| let id: String | |
| let userIDs: [String] | |
| } | |
| func printJSON<T: Encodable>(_ value: T) { | |
| let jsonData = try! JSONEncoder().encode(value) | |
| print(String(data: jsonData, encoding: .utf8)!) | |
| } | |
| // MARK: Phantom Types | |
| struct ID<Tag>: Codable, Hashable, ExpressibleByStringLiteral { | |
| public var rawValue: String | |
| public init(_ rawValue: String) { | |
| self.rawValue = rawValue | |
| } | |
| init(stringLiteral value: String) { | |
| self.rawValue = value | |
| } | |
| func encode(to encoder: Encoder) throws { | |
| var container = encoder.singleValueContainer() | |
| try container.encode(self.rawValue) | |
| } | |
| init(from decoder: Decoder) throws { | |
| self.init(try decoder.singleValueContainer().decode(String.self)) | |
| } | |
| } | |
| enum UserIDTag { } | |
| enum GroupIDTag { } | |
| typealias UserID = ID<UserIDTag> | |
| typealias GroupID = ID<GroupIDTag> | |
| struct StronglyTypedSocialGroup: Codable { | |
| let id: GroupID | |
| let userIDs: [UserID] | |
| } | |
| let example1 = SocialGroup( | |
| id: "GroupID1", | |
| userIDs: ["UserID1", "UserID2"] | |
| ) | |
| print("Example of using Codable with phantom type wrappers:") | |
| printJSON(example1) | |
| print() | |
| // MARK: Fixing Dictionary Encoding with Property Wrappers | |
| @propertyWrapper struct IDKeyed<Tag, Value: Codable>: Codable, ExpressibleByDictionaryLiteral { | |
| var wrappedValue: [ID<Tag>: Value] | |
| init(dictionaryLiteral elements: (ID<Tag>, Value)...) { | |
| wrappedValue = .init(uniqueKeysWithValues: elements) | |
| } | |
| func encode(to encoder: Encoder) throws { | |
| var container = encoder.singleValueContainer() | |
| let untaggedValue = wrappedValue.reduce(into: [String: Value]()) { untaggedDict, next in | |
| untaggedDict[next.key.rawValue] = next.value | |
| } | |
| try container.encode(untaggedValue) | |
| } | |
| init(from decoder: Decoder) throws { | |
| let untaggedValue = try decoder.singleValueContainer().decode([String: Value].self) | |
| self.wrappedValue = untaggedValue.reduce(into: [:]) { taggedDict, next in | |
| taggedDict[ID(next.key)] = next.value | |
| } | |
| } | |
| } | |
| struct WeaklyTypedExample: Codable { | |
| let dictionary: [String: Int] | |
| } | |
| let example2 = WeaklyTypedExample(dictionary: [ | |
| "UserID1": 5, | |
| "UserID2": 10 | |
| ]) | |
| print("Example of the default dictionary JSON encoding:") | |
| printJSON(example2) | |
| struct CustomKeyEncodingExample: Codable { | |
| let dictionary: [UserID: Int] | |
| } | |
| let example3 = CustomKeyEncodingExample(dictionary: [ | |
| "UserID1": 5, | |
| "UserID2": 10 | |
| ]) | |
| print("Example of the different encoding when keys are not Int or String:") | |
| printJSON(example3) | |
| struct PropertyWrapperFixExample: Codable { | |
| @IDKeyed var dictionary: [UserID: Int] | |
| } | |
| let example4 = PropertyWrapperFixExample(dictionary: [ | |
| "UserID1": 5, | |
| "UserID2": 10 | |
| ]) | |
| print("Example of matching the default encoding with a Property Wrapper:") | |
| printJSON(example4) | |
| print() | |
| // MARK: CodingKeyRepresentable | |
| struct AnyCodingKey: CodingKey { | |
| var stringValue: String | |
| var intValue: Int? | |
| init?(intValue: Int) { | |
| self.intValue = intValue | |
| self.stringValue = "\(intValue)" | |
| } | |
| init?(stringValue: String) { | |
| self.intValue = nil | |
| self.stringValue = stringValue | |
| } | |
| init<T: CodingKey>(_ key: T) { | |
| self.stringValue = key.stringValue | |
| self.intValue = key.intValue | |
| } | |
| init(_ int: Int) { | |
| self.init(intValue: int)! | |
| } | |
| init(_ string: String) { | |
| self.init(stringValue: string)! | |
| } | |
| } | |
| struct IDWithCodingKey<Tag>: Codable, Hashable, CodingKeyRepresentable, ExpressibleByStringLiteral { | |
| public var rawValue: String | |
| public init(_ rawValue: String) { | |
| self.rawValue = rawValue | |
| } | |
| init(stringLiteral value: String) { | |
| self.rawValue = value | |
| } | |
| func encode(to encoder: Encoder) throws { | |
| var container = encoder.singleValueContainer() | |
| try container.encode(self.rawValue) | |
| } | |
| init(from decoder: Decoder) throws { | |
| self.init(try decoder.singleValueContainer().decode(String.self)) | |
| } | |
| var codingKey: CodingKey { | |
| AnyCodingKey(rawValue) | |
| } | |
| init?<T: CodingKey>(codingKey: T) { | |
| self.rawValue = codingKey.stringValue | |
| } | |
| } | |
| typealias UserIDWithCodingKey = IDWithCodingKey<UserIDTag> | |
| struct CodingKeyFixExample: Codable { | |
| // No more property wrapper! | |
| let dictionary: [UserIDWithCodingKey: Int] | |
| } | |
| let example5 = CodingKeyFixExample(dictionary: [ | |
| "UserID1": 5, | |
| "UserID2": 10 | |
| ]) | |
| print("Example of matching the default encoding with CodingKeyRepresentable:") | |
| printJSON(example5) | |
| // Example of using Codable with phantom type wrappers: | |
| // {"id":"GroupID1","userIDs":["UserID1","UserID2"]} | |
| // | |
| // Example of the default dictionary JSON encoding: | |
| // {"dictionary":{"UserID1":5,"UserID2":10}} | |
| // Example of the different encoding when keys are not Int or String: | |
| // {"dictionary":["UserID1",5,"UserID2",10]} | |
| // Example of matching the default encoding with a Property Wrapper: | |
| // {"dictionary":{"UserID2":10,"UserID1":5}} | |
| // | |
| // Example of matching the default encoding with CodingKeyRepresentable: | |
| // {"dictionary":{"UserID1":5,"UserID2":10}} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment