この記事はEnigmo Advent Calendar 2018の23日目です
こんにちは。iOSチームでエンジニアをやっています。
Codable使ってますか?
iOSチームでは、 Alamofire + Codable で ネットワークまわりの実装を行なっています。
最初はいいのかわからなかったのですが、今ではなくてはならないものになっています。
すごく便利すぎて、Codable無しじゃ開発できない!そんな生活を送っています。
Codableについて軽く説明からの、実際使ってみて、Codableの得意なところと苦手なところを書いていこうと思います。
TL;DR
- Codable良いから使ってみて!
- Codableになれると、Enumをたくさん使うようになる
Codableとは
Swift4からFoundationに追加されたtypealiasです。 ( Codableは、プロトコルではありません )
Encodable
とDecodable
の二つプロトコルに準拠します。
https://developer.apple.com/documentation/swift/codable
Codable、何に使うの?
JSONを扱う際の、エンコード / デコード を Codableを使い、簡易に表現します
どうやって使えばいいの?
例えば
itunes.apple.com から取得できるJSONから今リリースしているアプリのVersion知りたい!という時に、使えます。
JSON:
{ "resultCount": 1, "results": [ { .... "sellerUrl": "https://www.buyma.com", "trackName": "BUYMA(バイマ) - 海外ファッション通販アプリ", "currentVersionReleaseDate": "2018-12-12T06:02:14Z", "version": "3.3.0", "minimumOsVersion": "10.0", ... } ] }
Codable:
struct AppInfoResponse: Codable { let results: [Results] struct Results: Codable { let version: String } }
すごい簡単ですね。
Codableの良さ
Foundation 純正
- 純正なので、Swiftのバージョンが上がった際に、オンタイムでアップデートされている
- サードパーティのライブラリの場合、そのライブラリのアップデート対応が終わるまでXcode/Swiftのバージョンを上げれない
JSONDecoderのカスタムもいける
- APIのレスポンス内のデータで、Date / DateTimeクラス が
2018-11-4 23:59
や2018-06-24T23:59:59+09:00
のように揃っていないケースがありますJSONDecoder
のdateDecodingStrategy
を.custom
にすることで、様々なケースのFormatに対応することができますdecoder.dateDecodingStrategy = .custom { let container = try $0.singleValueContainer() let string = try container.decode(String.self) let formatter = DateFormatter() /// Date format: ISO_8601 formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" if let date = formatter.date(from: string) { return date } formatter.dateFormat = "yyyy-MM-dd HH:mm" if let date = formatter.date(from: string) { return date } return Date() }
Codableの苦手なところ
nilで返さず、空文字で返すと失敗する
- URLがあるけど、URLになっていない
struct UserResponse: Codable { let name: String let imageUrl: URL? enum CodingKeys: String, CodingKey { case name case imageUrl = "image_url" } }
成功:
{ "name": "P", "image_url": "https://www.buyma.com/image" } </pre> 失敗: <pre> { "name": "P", "image_url": "" // 空文字 }
対応策
image_url
を 空文字ではなく、nil
にしてもらうinit(from decoder: Decoder)
を実装する
e.g.:
init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) imageUrl = try? container.decode(URL.self, forKey: .imageUrl) }
Arrayの中に、様々な Classが混ざっているケース
"topics"
の各Objectの"type"
を見ないといけない:
{ "topics": [ { "type": "sale", "title": "夏のセール開催中!", "products": [ ] }, { "type": "news", "title": "夏のセール開催中!", "image_url": "https://www.buyma.com/image", "link": "https://www.buyma.com/" }, { "type": "topic", "title": "韓国ブランド集めました!", "search_url": "https://www.buyma.com/search" } ] }
対応策
type
見るCodableを使って、一度どのtype
になるのかを判定してから再度デコードする
チームでCodableをどうやって使っているか
開発フロー
- APIをcURLで叩いてjsonを取得
- Codable準拠したstruct Responseを作成する
- cURLで取得したjsonファイルを使用してstruct Responseにデコードされるかテストを実装する
サンプルコード
ネットワーククライアントのデコード処理:
func decode(_ type: T.Type, from data: Data) -> T? { do { return try decoder.decode(type, from: data) } catch { print("---- API Parse Error ---") print(String(bytes: data, encoding: .utf8) ?? "") print("Error Description: \(error)") return nil } }
Codableができているかどうかのテスト:
class ResponseTests: TestCase { func testDecodeResponse() { guard let path = Bundle(for: type(of: self)).path(forResource: "ResponseSample/sample", ofType: "json"), let fileHandle = FileHandle(forReadingAtPath: path) else { fatalError() } guard let response = decoder.decode(SampleResponse.self, from: fileHandle.readDataToEndOfFile()) else { fatalError() } XCTAssertNotNil(response) } }