こんにちは!コネヒトでiOSエンジニアをやっていますyanamuraです。
コネヒトではAPIで取得したJSONをDecodeするのにHimotokiを使ってきましたが、Swift4でSwift.Decodableが追加されてからは新しいコードはSwift.Decodableを使っています。共存状態はのぞましくなく全てSwift.Decodableにしたいところですが、かなりの量があるので一気に変更するのが大変で影響範囲も大きいです。そのため、徐々に移行していく方針を取りました。
ステップ1
徐々に移行するためにまずAPI通信部分のユニットテストを用意しました。古いAPI周りはテストが書かれていなかったのでちょっと大変でしたが、これをしたことで変更に凡ミスが入らないという安心感がえられ、また、置き換え作業自体もコンパイルとテストが通っていれば実機で動かして確認する必要がなく効率的に行うことができました。
ステップ2
ここからHimotoki.DecodableをSwift.Decodableに置き換えていきます。
置き換える上で問題となってくるのが、以下の例のようにDecodableなstruct間で依存が発生しているパターンです。
struct Question: Himotoki.Decodable { let user: User // UserもHimotoki.Decodable let answers: [Answer] // AnswerもHimotoki.Decodable } struct Video: Himotoki.Decodable { let user: User let videoId: Int }
QuestionをSwift.Decodableにしようとすると、その子のUser, AnswerもSwift.Decodableにしなければならず、さらにUserがSwift.Decodableに変えると、それを使っているVideoもSwift.Decodableに変える・・というように芋づる式に変更が必要となり大変なことになります。
そこでこのような場合は、次のようなパターンで対応していきます。
case1: 親がない場合
Swift.Decodableに変更するだけでOK
case2: 親が一つしかない場合
子:Swift.Decodableに変更
親:
before
extension DailyMessageResponse: Himotoki.Decodable { static func decode(_ e: Extractor) throws -> DailyMessageResponse { return try DailyMessageContent( dailyMessage: e <| "daily_message", child: e <| "child" ) } }
after
// daily_messageだけSwift.Decodableにする extension DailyMessageResponse: Himotoki.Decodable { static func decode(_ e: Extractor) throws -> DailyMessageResponse { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 decoder.keyDecodingStrategy = .convertFromSnakeCase if let json = e.rawValue as? [String: Any], let dailyMessageJSON = json["daily_message"] { let dailyMessage = try decoder.decode(DailyMessage.self, from: try JSONSerialization.data(withJSONObject: dailyMessageJSON, options: [])) return try DailyMessageContent( dailyMessage: dailyMessage, child: e <| "child" ) } else { throw Himotoki.DecodeError.missingKeyPath("daily_message") } } }
case3: 親が複数ある場合
子:Himotoki.Decodable, Swift.Decodable両方に準拠する
extension User: Himotoki.Decodable { static func decode(_ e: Extractor) throws -> User { return try User( id: e <| "id", name: e <| "name" ) } } extension User: Swift.Decodable {}
親:
親はすぐに変更しなくても問題ない。
親を変更する場合は、親が1つのパターンを使って一つずつ変更する。
まとめ
これを地道に続けてると移行が完了します。
コネヒトで開発しているママリiOSアプリではようやく移行が完了し、トータルで20~30個くらいのPullRequestになりました。移行完了までかなり時間を要しましたが、コネヒトでは2週間に一日くらいの頻度で、丸一日は普段開発しているプロダクトの目標とは関係のない負債解消などの技術的なことを行うようにしていまして、これを活用してやりきることができました。
大規模な変更となりましたが、問題なく移行完了しました。1つだけリグレッションテストで不具合が見つかりヒヤリとしましたが、その不具合は唯一ユニットテストの書き漏れがあった箇所のコードで発生しておりユニットテストの重要さを改めて感じました。
コネヒトではこういった負債解消だけでなくSwiftUIの導入など技術的なチャレンジも盛り込みながら開発しています。もしご興味ありましたら一度話を聞きに来てください!