Codableいいよ!

この記事はEnigmo Advent Calendar 2018の23日目です

こんにちは。iOSチームでエンジニアをやっています。

Codable使ってますか?

iOSチームでは、 Alamofire + Codable で ネットワークまわりの実装を行なっています。

最初はいいのかわからなかったのですが、今ではなくてはならないものになっています。

すごく便利すぎて、Codable無しじゃ開発できない!そんな生活を送っています。

Codableについて軽く説明からの、実際使ってみて、Codableの得意なところと苦手なところを書いていこうと思います。

TL;DR

  • Codable良いから使ってみて!
  • Codableになれると、Enumをたくさん使うようになる

Codableとは

Swift4からFoundationに追加されたtypealiasです。 ( Codableは、プロトコルではありません )

EncodableDecodableの二つプロトコルに準拠します。

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:592018-06-24T23:59:59+09:00のように揃っていないケースがあります JSONDecoderdateDecodingStrategy.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をどうやって使っているか

開発フロー

  • APIcURLで叩いてjsonを取得
  • Codable準拠したstruct Responseを作成する
  • cURLで取得したjsonファイルを使用してstruct Responseにデコードされるかテストを実装する

サンプルコード

ネットワーククライアントのデコード処理:

func decode(_ type: T.Type, from data: Data) -&gt; 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)
    }

}

まとめ

  • Codableにすることで、CodingKeyに準拠したCodingKeysを書かないといけない手間はありますが、それを書いてもメリットが大きいです
  • 苦手はありますが、API設計に起因する部分が多いと思うので、チームでAPIを相談する際に、何が苦手なのかを伝えるといいと思います
  • SwiftyJson, ObjectMapperと使っていましたが、ほぼCodableに移せました