カテゴリー: iOS

Moya vs APIKit

はじめに

こんにちは。カイザーです。今回は、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

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

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共通

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

    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

    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と組み合わせると強力になることが分かります。

出力結果

以下のような出力用関数を用意しました。

    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共に、以下のように出力されました。

東京都 東京 の天気
今日 晴時々曇
最低気温 12℃
最高気温 20℃
----
明日 晴時々曇
----

さいごに

Swiftではプロトコルやジェネリクスのサポートが強力であるため、自前でも簡単にAPIクライアントを作成できます。しかし、実際に作ってみると、紹介したライブラリと同じような機能になることは多いのではないでしょうか?
そうであれば、MoyaやAPIKitの導入を検討してみても良いかもしれません。

おすすめ書籍

カイザー

シェア
執筆者:
カイザー
タグ: iOSSwiftXcode

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

2週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

4週間 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前