Slide 1

Slide 1 text

The Type-Safe World of Codable Codable͕ಋ͘ܕ҆શͳੈք Tatsuya Tanaka / ాதୡ໵ (@tattn) #tryswiftconf

Slide 2

Slide 2 text

• Tatsuya Tanaka / ాத ୡ໵ • Live in Tokyo, Japan • iOS Developer • Work at Yahoo! JAPAN Tatsuya Tanaka (@tattn) @tattn @tanakasan2525

Slide 3

Slide 3 text

Do you use Codable?

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Common usage Codable is commonly used to map JSON to type

Slide 8

Slide 8 text

Is it used only for that?

Slide 9

Slide 9 text

Codable can be used for 
 a variety of things! No

Slide 10

Slide 10 text

Codable makes your Swift more beautiful
 &
 type-safe

Slide 11

Slide 11 text

Convert any type to Data

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

I don't recommend that 
 you use the code as it is

Slide 14

Slide 14 text

The effective way to use Codable smartly Collaboration with Protocol

Slide 15

Slide 15 text

Protocol to convert from/to Data protocol DataConvertible { init(_ data: Data) throws func convertToData() throws -> Data }

Slide 16

Slide 16 text

typealias Codable = Decodable & Encodable

Slide 17

Slide 17 text

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) } }

Slide 18

Slide 18 text

Let's use the protocol

Slide 19

Slide 19 text

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")

Slide 20

Slide 20 text

More Tips: Add interfaces to UserDefaults protocol DataConvertibleStore { func set(_ value: DataConvertible, forKey key: String) throws func value(_ 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(_ type: T.Type = T.self, forKey key: String) -> T? { let data = self.data(forKey: key) return (try? data.map(T.init)).flatMap({ $0 }) } }

Slide 21

Slide 21 text

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")

Slide 22

Slide 22 text

It brings you an easy and 
 safe way of type conversion. Codable has good chemistry with protocol.

Slide 23

Slide 23 text

Default Encoder / Decoder • JSONEncoder / JSONDecoder • PropertyListEncoder / PropertyListDecoder

Slide 24

Slide 24 text

You can create a custom encoder / decoder

Slide 25

Slide 25 text

As an example, 
 I'll introduce my custom encoder / decoder

Slide 26

Slide 26 text

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)

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

Do you want to see the implementation?

Slide 29

Slide 29 text

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(keyedBy type: Key.Type) -> KeyedEncodingContainer { return KeyedEncodingContainer(KeyedContainer(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(_ value: T) throws -> Any { try value.encode(to: self) return storage.popContainer() } } extension DictionaryEncoder { open func encode(_ 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: 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(_ value: T, forKey key: Key) throws { encoder.codingPath.append(key) defer { encoder.codingPath.removeLast() } set(try encoder.box(value), forKey: key.stringValue) } func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { codingPath.append(key) defer { codingPath.removeLast() } return KeyedEncodingContainer(KeyedContainer(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(_ value: T) throws { encoder.codingPath.append(AnyCodingKey(index: count)) defer { encoder.codingPath.removeLast() } push(try encoder.box(value)) } func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { codingPath.append(AnyCodingKey(index: count)) defer { codingPath.removeLast() } return KeyedEncodingContainer(KeyedContainer(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(_ 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?

Slide 30

Slide 30 text

IMPOSSIBLE in this time Custom encoder/decoder tends to be long code...

Slide 31

Slide 31 text

Don't worry

Slide 32

Slide 32 text

https://github.com/tattn/MoreCodable I published the code!

Slide 33

Slide 33 text

Not only can Codable make it type-safe, 
 but also it can generalize type conversion

Slide 34

Slide 34 text

Codable is Hero Save the type- safe world!

Slide 35

Slide 35 text

Thank you!