はじめに
こんにちは。カイザーです。今回は、iOSでWebAPIクライアントを実装するときに使用できるライブラリである「Moya」と「APIKit」を比較して紹介したいと思います。
私は、普段APIKitを好んで使用しますが、Moyaの調査も兼ねて、比較したいと思います。
MoyaとAPIKitの概要
両者ともに、APIクライアントを実装するための、ラッパライブラリとなります。
Githubの比較
Moya | APIKit | |
リポジトリオーナ | Moya | ishikawa |
Star数 | 11.4k | 1.7k |
Swift | 5.1(v14.0.0-betaで対応) | 5.0(5.0.0で対応) |
コントリビュータ数 | 188 | 30 |
記事作成時点(2019年11月)では、このようになっています。初回コミット時期に大きな差はありませんが、Moyaの方がコントリビュータ数が多いです。
しかし、Code frequencyを見てみると、近年はどちらも大きな変更が加えられていないことが分かります。
記事作成時点では、どちらもメンテナンスされており、Swift 5に対応しています。
機能比較
Moya | APIKit | |
リクエスト定義 | enum+extensionもしくはenum+structで、指定プロトコルに従って定義 | struct+extensionで、指定プロトコルに従って定義 |
レスポンスJSONパーサ | 標準でDecodableをサポート | 標準はDictionaryに変換されるが、プロトコル実装によりDecodableサポート可 |
ラップしている通信クラス | Alamofire | URLSession |
Rx Extension | サポート | なし |
サンプルレスポンス | サポート | なし |
依存ライブラリ | Alamofire ReactiveMoya ReactiveSwift Result RxAtomic RxCocoa RxMoya RxRelay RxSwift | Result |
最も異なるのは、「ラップしている通信クラス」だと思います。MoyaはAlamofireをラップしているため、Alamofireの便利な機能や拡張を使用することができます。
また、Moyaは「サンプルレスポンス」も特徴的です。これは、実際に通信せずに、定義されたモックJSONを、コールバックするものです。UTやUIテストでのパターンもに使えそうです。
APIKitは、シンプルで軽量であることが特徴ですね。APIKitは最低限の機能しかありませんが、多くはプロトコル指向による設計となっているため、カスタマイズしやすくなっています。
Rxに依存したアーキテクチャを組み、自動テストにも取り組むつもりであれば、Moyaがおすすめですね。
一方で、スピード重視で最低限の機能を開発していきたい場合には、APIKitがおすすめです。
実装の比較
それでは、実装面を比較していきたいと思います。今回は、livedoor天気情報APIを使用して、東京の天気をログ出力してみます。
リクエスト定義
Moya
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import Foundation import Moya enum WeatherService { case forecast(city: String) } extension WeatherService: TargetType { var baseURL: URL { URL(string: "http://weather.livedoor.com/forecast/webservice/json")! } var path: String { "/v1" } var method: Moya.Method { .get } var sampleData: Data { "{pinpointLocations: [{link: \"http://weather.livedoor.com/area/forecast/4020200\",name: \"大牟田市\"},{link: \"http://weather.livedoor.com/area/forecast/4020300\",name: \"久留米市\"},{link: \"http://weather.livedoor.com/area/forecast/4020700\",name: \"柳川市\"},{link: \"http://weather.livedoor.com/area/forecast/4021000\",name: \"八女市\"},{link: \"http://weather.livedoor.com/area/forecast/4021100\",name: \"筑後市\"},{link: \"http://weather.livedoor.com/area/forecast/4021200\",name: \"大川市\"},{link: \"http://weather.livedoor.com/area/forecast/4021600\",name: \"小郡市\"},{link: \"http://weather.livedoor.com/area/forecast/4022500\",name: \"うきは市\"},{link: \"http://weather.livedoor.com/area/forecast/4022800\",name: \"朝倉市\"},{link: \"http://weather.livedoor.com/area/forecast/4022900\",name: \"みやま市\"},{link: \"http://weather.livedoor.com/area/forecast/4044700\",name: \"筑前町\"},{link: \"http://weather.livedoor.com/area/forecast/4044800\",name: \"東峰村\"},{link: \"http://weather.livedoor.com/area/forecast/4050300\",name: \"大刀洗町\"},{link: \"http://weather.livedoor.com/area/forecast/4052200\",name: \"大木町\"},{link: \"http://weather.livedoor.com/area/forecast/4054400\",name: \"広川町\"}],link: \"http://weather.livedoor.com/area/forecast/400040\",forecasts: [{dateLabel: \"今日\",telop: \"曇り\",date: \"2019-11-03\",temperature: {min: null,max: null},image: {width: 50,url: \"http://weather.livedoor.com/img/icon/8.gif\",title: \"曇り\",height: 31}},{dateLabel: \"明日\",telop: \"曇のち晴\",date: \"2019-11-04\",temperature: {min: {celsius: \"13\",fahrenheit: \"55.4\"},max: {celsius: \"22\",fahrenheit: \"71.6\"}},image: {width: 50,url: \"http://weather.livedoor.com/img/icon/12.gif\",title: \"曇のち晴\",height: 31}},{dateLabel: \"明後日\",telop: \"晴れ\",date: \"2019-11-05\",temperature: {min: null,max: null},image: {width: 50,url: \"http://weather.livedoor.com/img/icon/1.gif\",title: \"晴れ\",height: 31}}],location: {city: \"久留米\",area: \"九州\",prefecture: \"福岡県\"},publicTime: \"2019-11-03T21:00:00+0900\",copyright: {provider: [{link: \"http://tenki.jp/\",name: \"日本気象協会\"}],link: \"http://weather.livedoor.com/\",title: \"(C) LINE Corporation\",image: {width: 118,link: \"http://weather.livedoor.com/\",url: \"http://weather.livedoor.com/img/cmn/livedoor.gif\",title: \"livedoor 天気情報\",height: 26}},title: \"福岡県 久留米 の天気\",description: {text: \" 九州北部地方は、気圧の谷の影響により曇りで雨や雷雨となっている所があります。 3日の九州北部地方は、気圧の谷や湿った空気の影響により概ね曇りで雨や雷雨の所があるでしょう。 4日の九州北部地方は、高気圧に覆われて晴れとなりますが、気圧の谷や寒気の影響により曇りで雨や雷雨の所があるでしょう。 波の高さは、対馬海峡では、3日は1.5メートル、4日は2.5メートルでしょう。九州西海上では、3日は1メートル、4日は2.5メートルでしょう。豊後水道では3日は1メートル、4日は1.5メートルでしょう。 福岡県の内海は、瀬戸内側では、3日は0.5メートル、4日は1メートルでしょう。有明海では、3日と4日は0.5メートルでしょう。 <天気変化等の留意点> 特記事項はありません。\",publicTime: \"2019-11-03T21:32:00+0900\"}}".data(using: .utf8)! } var task: Task { switch self { case .forecast(let city): return .requestParameters(parameters: ["city":city], encoding: URLEncoding.queryString) } } var headers: [String : String]? { ["Content-type": "application/json"] } } |
APIKit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | import Foundation import APIKit struct ForecastRequest: Request { let baseURL = URL(string: "http://weather.livedoor.com/forecast/webservice/json")! let method = HTTPMethod.get let path = "/v1" let headerFields = ["Content-type": "application/json", "Accept":"application/json"] typealias Response = Weather var queryParameters: [String : Any]? { ["city":city] } var dataParser: DataParser { DecodableDataParser() } // リクエストパラメータ let city: String } // JSONDecoderの実装 extension Request where Response: Decodable { internal func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response { guard let data = object as? Data else { throw ResponseError.unexpectedObject(object) } return try JSONDecoder().decode(Response.self, from: data) } } // JSONDecoderに渡す際、Dictionaryではなくdataをそのまま渡すための実装 class DecodableDataParser: DataParser { var contentType: String? = "application/json" func parse(data: Data) throws -> Any { data } } |
基本的にどちらも定義プロパティは似ていますが、異なる点について紹介していきます。
- 送信パラメータは、Moyaではenumの引数に設定するのに対し、APIKitではstructのプロパティとして設定します。
- Moyaはenum caseをAPIの本数文増やし、extensionで分岐しながら定義していきますが、このままだと分岐だらけになってしまいます。そのため、APIKitのように、APIごとにstructで実装することも可能です。
- Moyaではリクエスト定義時にレスポンス型は定義しませんが、APIKitではtypealiasで設定します。
- APIKitはDecodableは標準でサポートされていないため、JSONDecoderを使用するDataParserを定義しています。
レスポンスstruct
Moya, APIKit共通
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | struct Weather: Codable { var forecasts: [Forecast] var title: String } struct Forecast: Codable { var date: String var dateLabel: String var telop: String var temperature: TemperatureRange } struct TemperatureRange: Codable { var min: Temperature? var max: Temperature? } struct Temperature: Codable { var celsius: String var fahrenheit: String } |
今回は、Moya, APIKit共にJSONDecoderを使用してパースするため、レスポンスstructの実装は共通となります。
リクエスト送信
Moya
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | func moya() { let providor = MoyaProvider<WeatherService>() providor.request(.forecast(city: "130010")) { result in switch result { case let .success(response): guard let weather = try? response.map(Weather.self) else { return } self.printWeather(weather) case let .failure(error): print(error) } } } |
APIKit
1 2 3 4 5 6 7 8 9 10 | func apiKit() { Session.send(ForecastRequest(city: "130010")) { result in switch result { case .success(let response): self.printWeather(response) case .failure(let error): print(error) } } } |
こちらも似ていますね。
Moyaは、responseに
map(Decodable.Protocol)
が実装されており、ここにDecodableの型を指定するだけで、パースされます。
このパース部分が隠蔽されていないことが気になりますが、ここがRxでチェインされるネットワーク層であれば、うまく隠蔽できると思います。
この点からも、MoyaはRxと組み合わせると強力になることが分かります。
出力結果
以下のような出力用関数を用意しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | func printWeather(_ weather: Weather) { print(weather.title) weather.forecasts.forEach { forecast in print("\(forecast.dateLabel) \(forecast.telop)") if let minTemp = forecast.temperature.min { print("最低気温 \(minTemp.celsius)℃") } if let maxTemp = forecast.temperature.max { print("最高気温 \(maxTemp.celsius)℃") } print("----") } } |
Moya, APIKit共に、以下のように出力されました。
1 2 3 4 5 6 7 | 東京都 東京 の天気 今日 晴時々曇 最低気温 12℃ 最高気温 20℃ ---- 明日 晴時々曇 ---- |
さいごに
Swiftではプロトコルやジェネリクスのサポートが強力であるため、自前でも簡単にAPIクライアントを作成できます。しかし、実際に作ってみると、紹介したライブラリと同じような機能になることは多いのではないでしょうか?
そうであれば、MoyaやAPIKitの導入を検討してみても良いかもしれません。