Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Type-Safe World of Codable

The Type-Safe World of Codable

The Type-Safe World of Codable
Codableが導く型安全な世界

try! Swift Tokyo 2018 (#tryswiftconf)
https://www.tryswift.co/events/2018/tokyo/en/

# Source code
https://github.com/tattn/MoreCodable
https://github.com/tattn/DataConvertible

Tatsuya Tanaka

March 02, 2018
Tweet

More Decks by Tatsuya Tanaka

Other Decks in Programming

Transcript

  1. • Tatsuya Tanaka / ాத ୡ໵ • Live in Tokyo,

    Japan • iOS Developer • Work at Yahoo! JAPAN Tatsuya Tanaka (@tattn) @tattn @tanakasan2525
  2. About Codable It's a protocol added in Xcode 9.0.
 


    We can use Codable after 
 Swift 3.2 / 4.0. IUUQTEFWFMPQFSBQQMFDPNEPDVNFOUBUJPOGPVOEBUJPOBSDIJWFT@BOE@TFSJBMJ[BUJPOFODPEJOH@BOE@EFDPEJOH@DVTUPN@UZQFT
  3. Support encoding & decoding any type struct User: Codable {

    let id: String let name: String } let user = User(id: "abc", name: "Tatsuya Tanaka") let data = try! JSONEncoder().encode(user) let json = String(data: data, encoding: .utf8)! print(json) // {"id": "abc", "name": "Tatsuya Tanaka"} Encode
  4. Support encoding & decoding any type struct User: Codable {

    let id: String let name: String } let json = """ {"id": "abc", "name": "Tatsuya Tanaka"} """.data(using: .utf8)! let user = try! JSONDecoder().decode(User.self, from: json) print(user) // User(id: "abc", name: "Tatsuya Tanaka") Decode
  5. Support encoding & decoding any type struct User: Codable {

    let id: String let name: String } let user = User(id: "abc", name: "Tatsuya Tanaka") let data: Data = try! JSONEncoder().encode(user) let json = String(data: data, encoding: .utf8)! print(json) // {"id": "abc", "name": "Tatsuya Tanaka"} Encode
  6. Protocol to convert from/to Data protocol DataConvertible { init(_ data:

    Data) throws func convertToData() throws -> Data }
  7. Protocol to convert to Data extension DataConvertible where Self: Decodable

    { init(_ data: Data) throws { self = try JSONDecoder().decode(Self.self, from: data) } } extension DataConvertible where Self: Encodable { func convertToData() throws -> Data { return try JSONEncoder().encode(self) } }
  8. Looks good struct User: Codable, DataConvertible { let id: String

    let name: String } let user = User(id: "abc", name: "Tatsuya Tanaka") let data: Data = try! user.convertToData() try! User(data) // User(id: "abc", name: "Tatsuya Tanaka")
  9. More Tips: Add interfaces to UserDefaults protocol DataConvertibleStore { func

    set(_ value: DataConvertible, forKey key: String) throws func value<T: DataConvertible>(_ type: T.Type, forKey key: String) -> T? } extension UserDefaults: DataConvertibleStore { func set(_ value: DataConvertible, forKey key: String) throws { let data = try value.convertToData() set(data, forKey: key) } func value<T: DataConvertible>(_ type: T.Type = T.self, forKey key: String) -> T? { let data = self.data(forKey: key) return (try? data.map(T.init)).flatMap({ $0 }) } }
  10. So cooool let user = User(id: "abc", name: "Tatsuya Tanaka")

    let userDefaults = UserDefaults.standard try! userDefaults.set(user, forKey: "user") let savedUser = userDefaults.value(User.self, forKey: "user")
  11. It brings you an easy and 
 safe way of

    type conversion. Codable has good chemistry with protocol.
  12. DictionaryEncoder / Decoder let user = User(name: "Tatsuya Tanaka", age:

    24) var encoder = DictionaryEncoder() let dictionary: [String: Any] = try! encoder.encode(user) print(dictionary["name"] as! String) // Tatsuya Tanaka print(dictionary["age"] as! Int) // 24 var decoder = DictionaryDecoder() let user = try! decoder.decode(User.self, from: dictionary) print(user) // User(name: "Tatsuya Tanaka", age: 24)
  13. URLQueryItemsEncoder struct Parameter: Codable { let query: String let limit:

    Int } let parameter = Parameter(query: "Ͷ͜", limit: 20) var encoder = URLQueryItemsEncoder() let items: [URLQueryItem] = try! encoder.encode(parameter) print(params[0].name) // query print(params[0].value) // Ͷ͜ var urlComponents = URLComponents(string: "https://***.com")! urlComponents.queryItems = items print(urlComponents.url!) // https://***.com?query=%E3%81%AD%E3%81%93&limit=20
  14. Implementation of DictionaryEncoder import Foundation open class DictionaryEncoder: Encoder {

    open var codingPath: [CodingKey] = [] open var userInfo: [CodingUserInfoKey: Any] = [:] private var storage = Storage() public init() {} open func container<Key: CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> { return KeyedEncodingContainer(KeyedContainer<Key>(encoder: self, codingPath: codingPath)) } open func unkeyedContainer() -> UnkeyedEncodingContainer { return UnkeyedContanier(encoder: self, codingPath: codingPath) } open func singleValueContainer() -> SingleValueEncodingContainer { return UnkeyedContanier(encoder: self, codingPath: codingPath) } private func box<T: Encodable>(_ value: T) throws -> Any { try value.encode(to: self) return storage.popContainer() } } extension DictionaryEncoder { open func encode<T: Encodable>(_ value: T) throws -> [String: Any] { do { return try castOrThrow([String: Any].self, try box(value)) } catch (let error) { throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-evel \(T.self) did not encode any values.", underlyingError: error) ) } } } extension DictionaryEncoder { private class KeyedContainer<Key: CodingKey>: KeyedEncodingContainerProtocol { private var encoder: DictionaryEncoder private(set) var codingPath: [CodingKey] private var storage: Storage init(encoder: DictionaryEncoder, codingPath: [CodingKey]) { self.encoder = encoder self.codingPath = codingPath self.storage = encoder.storage storage.push(container: [:] as [String: Any]) } deinit { guard let dictionary = storage.popContainer() as? [String: Any] else { assertionFailure(); return } storage.push(container: dictionary) } private func set(_ value: Any, forKey key: String) { guard var dictionary = storage.popContainer() as? [String: Any] else { assertionFailure(); return } dictionary[key] = value storage.push(container: dictionary) } func encodeNil(forKey key: Key) throws {} func encode(_ value: Bool, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: Int, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: Int8, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: Int16, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: Int32, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: Int64, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: UInt, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: UInt8, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: UInt16, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: UInt32, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: UInt64, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: Float, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: Double, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode(_ value: String, forKey key: Key) throws { set(value, forKey: key.stringValue) } func encode<T: Encodable>(_ value: T, forKey key: Key) throws { encoder.codingPath.append(key) defer { encoder.codingPath.removeLast() } set(try encoder.box(value), forKey: key.stringValue) } func nestedContainer<NestedKey: CodingKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> { codingPath.append(key) defer { codingPath.removeLast() } return KeyedEncodingContainer(KeyedContainer<NestedKey>(encoder: encoder, codingPath: codingPath)) } func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { codingPath.append(key) defer { codingPath.removeLast() } return UnkeyedContanier(encoder: encoder, codingPath: codingPath) } func superEncoder() -> Encoder { return encoder } func superEncoder(forKey key: Key) -> Encoder { return encoder } } private class UnkeyedContanier: UnkeyedEncodingContainer, SingleValueEncodingContainer { var encoder: DictionaryEncoder private(set) var codingPath: [CodingKey] private var storage: Storage var count: Int { return (storage.last as? [Any])?.count ?? 0 } init(encoder: DictionaryEncoder, codingPath: [CodingKey]) { self.encoder = encoder self.codingPath = codingPath self.storage = encoder.storage storage.push(container: [] as [Any]) } deinit { guard let array = storage.popContainer() as? [Any] else { assertionFailure(); return } storage.push(container: array) } private func push(_ value: Any) { guard var array = storage.popContainer() as? [Any] else { assertionFailure(); return } array.append(value) storage.push(container: array) } func encodeNil() throws {} func encode(_ value: Bool) throws {} func encode(_ value: Int) throws { push(value) } func encode(_ value: Int8) throws { push(value) } func encode(_ value: Int16) throws { push(value) } func encode(_ value: Int32) throws { push(value) } func encode(_ value: Int64) throws { push(value) } func encode(_ value: UInt) throws { push(value) } func encode(_ value: UInt8) throws { push(value) } func encode(_ value: UInt16) throws { push(value) } func encode(_ value: UInt32) throws { push(value) } func encode(_ value: UInt64) throws { push(value) } func encode(_ value: Float) throws { push(value) } func encode(_ value: Double) throws { push(value) } func encode(_ value: String) throws { push(value) } func encode<T: Encodable>(_ value: T) throws { encoder.codingPath.append(AnyCodingKey(index: count)) defer { encoder.codingPath.removeLast() } push(try encoder.box(value)) } func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey { codingPath.append(AnyCodingKey(index: count)) defer { codingPath.removeLast() } return KeyedEncodingContainer(KeyedContainer<NestedKey>(encoder: encoder, codingPath: codingPath)) } func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { codingPath.append(AnyCodingKey(index: count)) defer { codingPath.removeLast() } return UnkeyedContanier(encoder: encoder, codingPath: codingPath) } func superEncoder() -> Encoder { return encoder } } } final class Storage { private(set) var containers: [Any] = [] var count: Int { return containers.count } var last: Any? { return containers.last } func push(container: Any) { containers.append(container) } @discardableResult func popContainer() -> Any { precondition(containers.count > 0, "Empty container stack.") return containers.popLast()! } } struct AnyCodingKey : CodingKey { public var stringValue: String public var intValue: Int? public init?(stringValue: String) { self.stringValue = stringValue self.intValue = nil } public init?(intValue: Int) { self.stringValue = "\(intValue)" self.intValue = intValue } init(index: Int) { self.stringValue = "Index \(index)" self.intValue = index } static let `super` = AnyCodingKey(stringValue: "super")! } public enum MoreCodableError: Error { case cast case unwrapped case tryValue } func castOrThrow<T>(_ resultType: T.Type, _ object: Any, error: Error = MoreCodableError.cast) throws -> T { guard let returnValue = object as? T else { throw error } return returnValue } extension Optional { func unwrapOrThrow(error: Error = MoreCodableError.unwrapped) throws -> Wrapped { guard let unwrapped = self else { throw error } return unwrapped } } extension Dictionary { func tryValue(forKey key: Key, error: Error = MoreCodableError.tryValue) throws -> Value { guard let value = self[key] else { throw error } return value } } M ake sense?
  15. Not only can Codable make it type-safe, 
 but also

    it can generalize type conversion