Slide 1

Slide 1 text

今から始めるCodable移⾏ ⾼⽊ 豪 ⽇本経済新聞社 Otemachi.swift #

Slide 2

Slide 2 text

‣ Go Takagi • 新卒1年⽬、新⽶ • 好きな⾔語: Go • @shimastriper ‣ 最近の悩み • iMacPro貸与されてから快適で帰る気が起きない • 新卒がiMacPro使えるイイカイシャデスヨ!!(≧∇≦)b I am... 2

Slide 3

Slide 3 text

今⽇は少し泥臭い話をします 3

Slide 4

Slide 4 text

4 皆さんCodable使ってますか?

Slide 5

Slide 5 text

‣ EntityとWebAPI(JSON)を簡潔にマッピング Codable 5 https://developer.apple.com/documentation/foundation/jsondecoder struct GroceryProduct: Codable { var name: String var points: Int var description: String? } let json = """ { "name": "Durian", "points": , "description": "A fruit with a distinctive scent." } """.data(using: .utf )! let decoder = JSONDecoder() let product = try decoder.decode(GroceryProduct.self, from: json) print(product.name) // Prints "Durian"

Slide 6

Slide 6 text

‣ EntityとWebAPI(JSON)を簡潔にマッピング Codable 6 https://developer.apple.com/documentation/foundation/jsondecoder 相互にデータを変換!! Entity JSON struct GroceryProduct: Codable { var name: String var points: Int var description: String? } let json = """ { "name": "Durian", "points": , "description": "A fruit with a distinctive scent." } """.data(using: .utf )! let decoder = JSONDecoder() let product = try decoder.decode(GroceryProduct.self, from: json) print(product.name) // Prints "Durian"

Slide 7

Slide 7 text

7 ガンガン使っていきましょう!!

Slide 8

Slide 8 text

が、、、 8 既存からの移⾏は結構⼤変...

Slide 9

Slide 9 text

‣ APIの構造がEntityと1:1対応してないとCodableは⾟い • 前処理: 値を変換して⼊れるとか • 値を演算してから⼊れる • 複数のキーを利⽤ / 失敗を握りつぶす • どちらかのキーに⼊っているものだけ利⽤ • 変換失敗する要素は握りつぶす配列 • キーが不定: このキーないとき無視してnil代⼊ • ⼤きいJSONの⼀部だけEntityにする • 1つのリクエストから複数のEntityに分割 • どういうところが⼤変か 9

Slide 10

Slide 10 text

Decode/Encodeを⾃前で書くと... 10 struct GroceryProduct: Codable { let name: String let points: Int enum CodingKeys: String, CodingKey { case name case points } } extension GroceryProduct: Decodable { init(from: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.name = try values.decode(String.self, forKey: .name) self.points = try values.decode(Int.self, forKey: .points) } } extension GroceryProduct: Encodable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.name, forKey: .name) try container.encode(self.points, forKey: .points) } }

Slide 11

Slide 11 text

Decode/Encodeを⾃前で書くと... 11 struct GroceryProduct: Codable { let name: String let points: Int enum CodingKeys: String, CodingKey { case name case points } } extension GroceryProduct: Decodable { init(from: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.name = try values.decode(String.self, forKey: .name) self.points = try values.decode(Int.self, forKey: .points) } } extension GroceryProduct: Encodable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.name, forKey: .name) try container.encode(self.points, forKey: .points) } } 冗⻑な記述を 書くのを避けたい 処理する段階に応じて できるだけ冗⻑さを避ける

Slide 12

Slide 12 text

‣ API側で担保すべきことも往々にしてあるが... • Androidと共通利⽤してるとAPI側を直すのも⼀苦労 • init(from Decoder)/ func encode(to: Encoder)の⽅法を
 毎回書くと冗⻑...... • 簡潔にできることはないか? ‣ ケースに基づく対応⽅法を紹介 • ⽇経の90EntitiesをCodable対応して知⾒をまとめました • もともと⽇経は github.com/JohnSundell/Unbox でJSONを扱ってました • クライアント側単体でなるべく冗⻑さを回避して旨味を享受する 歴史ある仕様に柔軟に対応しないといけないとき 12

Slide 13

Slide 13 text

‣ Codableとは ‣ JSONDecoder / JSONEncoder で全体の⼀貫した設定 • snake_case、Date型、pretty format... ‣ キーのマッピングはCodingKeysで指定 • 丁寧に指定しておくと簡潔になることも ‣ それでもダメならdecode / encode⽅法を⾃前で書く ‣ ハマりどころ‧懸念 • 配列の要素を操作する場合は気をつけないと無限ループする • Performance Issueとか 検討⼿順 (⾚字に⾄る前に解決したいのが肝です) 13

Slide 14

Slide 14 text

‣ データ表現を相互に変換するためのProtocol • A type that can convert itself into and out of an external representation. • 今回はJSON • 標準の型はもちろんCodableに準拠するデータ構造で
 フィールドを構成していればCodableにできる • OptionalにすればJSONのnullもマッピング可能 Codableとは 14 struct GroceryProduct: Codable { let name: String let points: Int let description: String? let myStruct: MyStruct } struct MyStruct: Codable { var name: String } let json = """ { "name": "Durian", "points": , "description": "A fruit with a distinctive scent." "myStruct": { "name": "Durian" } } """.data(using: .utf )!

Slide 15

Slide 15 text

Decode/Encodeを⾃前で書くと... 15 struct GroceryProduct: Codable { let name: String let points: Int enum CodingKeys: String, CodingKey { case name case points } } extension GroceryProduct: Decodable { init(from: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.name = try values.decode(String.self, forKey: .name) self.points = try values.decode(Int.self, forKey: .points) } } extension GroceryProduct: Encodable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.name, forKey: .name) try container.encode(self.points, forKey: .points) } } 冗⻑な記述を 書くのを避けたい 処理する段階に応じて できるだけ冗⻑さを避ける

Slide 16

Slide 16 text

struct GroceryProduct: Codable { let name: String let points: Int let description: String? let myStruct: MyStruct } struct MyStruct: Codable { var name: String } let json = """ { "name": "Durian", "points": , "description": "A fruit with a distinctive scent." "myStruct": { "name": "Durian" } } """.data(using: .utf )! ‣ keyDecodingStrategy / keyEncodingStrategy • JSONのKeyの記法を変換してからマップできる • camelCase, snake_caseに限らず独⾃定義の変換も可能 • Swift . (Xcode . )から利⽤可能 DecodeやEncodeのデータ全体に対する設定① 16 Ex. JSONDecoder().keyDecodingStrategy = .snakeCaseToCamelCase

Slide 17

Slide 17 text

struct GroceryProduct: Codable { let name: String let points: Int let description: String? let myStruct: MyStruct } struct MyStruct: Codable { var name: String } let json = """ { "name": "Durian", "points": , "description": "A fruit with a distinctive scent." "my_struct": { "name": "Durian" } } """.data(using: .utf )! ‣ keyDecodingStrategy / keyEncodingStrategy • JSONのKeyの記法を変換してからマップできる • camelCase, snake_caseに限らず独⾃定義の変換も可能 • Swift . (Xcode . )から利⽤可能 DecoderやEncodeでデータ全体に対する設定① 17 Ex. JSONDecoder().keyDecodingStrategy = .snakeCaseToCamelCase ࣗಈͰϚοϐϯά

Slide 18

Slide 18 text

‣ 他にも • DateDecodingStrategy / DateEncodingStrategy • Date型のフォーマットを指定できる • DataDecodingStrategy / DataEncodingStrategy • バイナリ型のフォーマットを指定 • OutputFormatting • JSONEncoderからpretty formatで出⼒ • NonConformingFloatEncodingStrategy • 浮動⼩数点数を扱う DecodeやEncodeでデータ全体に対する設定② 18

Slide 19

Slide 19 text

‣ Decode/Encodeのキーとフィールドが⼀致しない際利⽤ • `enum CodingKeys`を定義するとそれに従う • `case field = "jsonKey"`でマップ • 基本的にcaseはフィールド名と1:1で対応させる • それ以上でも以下でもdecode⽅法とencode⽅法の
 実装が求められるようになる キー名の違いはCodingKeysで解決 19 struct GroceryProduct: Codable { let name: String let points: Int enum CodingKeys: String, CodingKey { case name = "otherName" case points = "otherPoints" } } let json = """ { "other_name": "Durian", "other_points": } """.data(using: .utf )!

Slide 20

Slide 20 text

‣ 追加で使うキーは別のCodingKeysにする • ⾃前で書く必要がない処理を省略できる フィールドと対応させることの効能 20 struct GroceryProduct: Codable { let name: String let points: Int enum CodingKeys: String, CodingKey { case name case points } enum AdditionalCodingKeys: String, CodingKey { case usingOnlyDecode } } init(from decoder: Decoder) のみで利⽤ encodeは ⾃動推論してくれる

Slide 21

Slide 21 text

‣ decodeやencode⽅法を⾃分で書く それでも前処理したかったら 21 extension GroceryProduct: Decodable { init(from: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.name = try values.decode(String.self, forKey: .name) self.points = try values.decode(Int.self, forKey: .points) } } extension GroceryProduct: Encodable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.name, forKey: .name) try container.encode(self.points, forKey: .points) } }

Slide 22

Slide 22 text

tips: 配列のDecodeできない要素を握りつぶす 22 struct GroceryProductList: Decodable { let products: [GroceryProduct] init(from decoder: Decoder) throws { var products: [GroceryProduct] = [] var unkeyedContainer = try decoder.unkeyedContainer() while !unkeyedContainer.isAtEnd { let product = try unkeyedContainer.decode(GroceryProduct.self) products.append(product) } self.products = products } } ‣ 配列要素のDecode失敗を握りつぶしたい 配列の要素をDecode ここでtry catchすればいいのでは?

Slide 23

Slide 23 text

実は無限ループが起きる 23 struct GroceryProductList: Decodable { let products: [GroceryProduct] init(from decoder: Decoder) throws { var products: [GroceryProduct] = [] var unkeyedContainer = try decoder.unkeyedContainer() while !unkeyedContainer.isAtEnd { let product = try unkeyedContainer.decode(GroceryProduct.self) products.append(product) } self.products = products } } ‣ Containerのループのインクリメントがされない • 同じパースをし続けて⼀⽣失敗 decode成功しないと
 ループが進まない! ref: https://github.com/apple/swift/blob/master/stdlib/public/Darwin/Foundation/JSONEncoder.swift#L guard let decoded = try self.decoder.unbox... throw DecodingError.valueNotFound... } self.currentIndex += swift側の実装

Slide 24

Slide 24 text

nilを成功扱いするstructでwrapすると対処可能 24 ref: https://github.com/JohnSundell/Codextended/pull/ private struct IgnoreFailureDecodable: Decodable { let element: T? init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.element = try? container.decode(T.self) } } let container = try self.container(keyedBy: CodingKeys.self) let decodedArray = try container.decode([IgnoreFailureDecodable].self, forKey: .array) return decodedArray.compactMap({ $0.element }) 失敗してもnil扱いで Parse成功するstruct nilを消去

Slide 25

Slide 25 text

Performance Issue 25 ‣ CodableはPerformanceに直結して響く • decode / encodeを⾃前で書くか推論させるか • データ構造の複雑さによって
 どっちのほうが早いか安定してない記事がちらほら... • ⽇経のEntityは後者のほうが10倍早かった (実測値) • JSONにEntityと関係ないデータがたくさんあるか • JSON→Dictionary→Entityと変換して必要箇所を参照するほうが早い • 移⾏したら忘れずにInstrumentsで測定 !! decodeを定義 Codableが推論

Slide 26

Slide 26 text

‣ Codableは便利だから使ってほしい • しかし、移⾏する場合WebAPI次第で結構苦労する ‣ 冗⻑な記述を避ける⼿順 • 全体のParseするルールの⼀貫性を設定 • snake_caseをここで解決できることは嬉しい • CodingKeysをFieldと1:1対応させる • ⾃前で書く必要があるdecode/encodeを記述 • Parseできない配列要素を握りつぶす場合テクニックが要る • Performanceにsensitiveに影響、Instrumentsは忘れずに まとめ 26