NSBlogger

意識高いブログ

RxMoya + Codableで実現するリアクティブなAPIクライアント

Moya

最近MoyaというAPIクライアントを使っています。なかなか良さげで使い勝手も悪くありません。
github.com

Alamofireベースで作られており、Moyaを入れるだけでいい感じのAPIクライアントがすぐに手に入ります。
また、Rx拡張もされているのでRxSwiftなどとの相性も良いでしょう。

Codable

Swift4から使えるprotocolに「Codable」というのがあります。これを利用するとAPIから取得したデータ(JSON)のパースが非常に簡単に行なえます。

JSONとオブジェクトのマッピングSwiftyJSONObjectMapperなどのライブラリを使うことが多いですが、Codableを使えばもう必要ありません。



Moya + CodableでQiitaの情報を取得する

簡単な例としてQiitaの情報をMoyaとCodableを使って取得してみました。
GitHubにサンプルのプロジェクトをまるごと置いているので参考にしてください。

Codableに準拠したModelの定義

APIのレスポンスに従ってModelを作成します。structで定義してCodableに準拠させるだけ。
準拠させるといっても、内部で持つプロパティの型がCodableに準拠している場合は「Codable」を書いてあげるだけで済みます。
プロパティの型に独自のクラスが混ざっている場合は、そのクラスもCodableに準拠させる必要があります。

struct User: Codable {
    let description: String?
    let facebookID: String?
    let followeesCount: Int
    let followersCount: Int
    let githubLoginName: String?
    let id: String
    let itemsCount: Int
    let linkedinID: String?
    let location: String?
    let name: String
    let organization: String?
    let permanentID: Int
    let profileImageURL: String
    let twitterScreenName: String?
    let websiteURL: String?
    
    private enum CodingKeys: String, CodingKey {
        case description
        case facebookID = "facebook_id"
        case followeesCount = "followees_count"
        case followersCount = "followers_count"
        case githubLoginName = "github_login_name"
        case id
        case itemsCount = "items_count"
        case linkedinID = "linkedin_id"
        case location
        case name
        case organization
        case permanentID = "permanent_id"
        case profileImageURL = "profile_image_url"
        case twitterScreenName = "twitter_screen_name"
        case websiteURL = "website_url"
    }
}
CodingKeys

CodingKeysでは実際のAPIのレスポンスに使われているキー名とプロパティ名とのマッピングを行います。
キー名とプロパティ名が同じであれば記載する必要はありません。
case文はプロパティ名の数だけすべて羅列する必要がありました。

MoyaでAPIの定義

Moyaを使ってAPIの定義をします。Moyaではenumを使ってAPIを定義します。
そして「TargetType」というprotocolに準拠させることでリクエスト情報を明確にします。
プロジェクトの規模やAPI設計に応じて、enumの定義は慎重に行いましょう。case文が増えるとTargetTypeに準拠させる際に、switch文だらけになり見通しが悪くなってしまうことがあります。

enum UserAPI {
    case fetch
}

// MARK: - TargetType
extension UserAPI: TargetType {
    var baseURL: URL {
        return URL(string: "https://qiita.com")!
    }
    
    var path: String {
        return "/api/v2/users"
    }
    
    var method: Moya.Method {
        return .get
    }
    
    var sampleData: Data {
        let path = Bundle.main.path(forResource: "sample", ofType: "json")!
        return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile()
    }
    
    var task: Task {
        return .requestParameters(parameters: ["page":1], encoding: URLEncoding.default)
    }
    
    var headers: [String : String]? {
        return nil
    }
}

だいたい読んで字のごとく、リクエスト先やメソッドタイプ、HTTPHeaderなどを定義します。
Taskは最近のアップデートで大幅に変更が加えられました。すでにMoyaを導入している方は要チェックです。

sampleDataでStubを作成

sampleDataに指定した情報をダミーのデータとして返すことができます。
APIの実装待ちや手元で任意のデータで確認したいときなどに便利ですよ。詳細の実装は後述。

MoyaProviderの作成

MoyaでAPI通信するためには「MoyaProvider」を作成する必要があります。

fileprivate let provider: MoyaProvider<UserAPI> = {
        let stubClosure = { (target: UserAPI) -> StubBehavior in
            return .never
        }
        let networkLoggerPlugin = NetworkLoggerPlugin(cURL: true)
        let plugins = [networkLoggerPlugin]
        return MoyaProvider<UserAPI>(stubClosure: stubClosure, plugins: plugins)
    }()

Genericsで先ほど作成した「UserAPI」を指定します。あとは必要に応じてStubやPluginの設定を行えば完成。

StubBehavior

TargetTypeで指定した「sampleData」はこのStubBehaviorで利用されます。
StubBehaviorには下記の3種類が用意されているので、場面に応じて使い分けてみましょう。

StubBehavior 振る舞い
.never 通常のAPI通信をする
.immediate sampleDataをすぐに返す
.delay(x) x秒後にsampleDataを返す

指定した秒数分待ったあとにレスポンスを返すこともできるので、ローディングの様子や実際に近い動きを試せますね。

Plugin

プラグインは様々な種類がありますが、上記の例はAPI通信する際にcurlコマンドをログにはいてくれます。

$ curl -v \
	-H "Accept-Language: en;q=1.0, ja-JP;q=0.9" \
	-H "User-Agent: RxMoya+Codable/1.0 (com.imairi.RxMoya-Codable; build:1; iOS 11.0.0) Alamofire/4.5.1" \
	-H "Accept-Encoding: gzip;q=1.0, compress;q=0.5" \
	"https://qiita.com/api/v2/users?page=1"

MoyaはAlamofireベースで作られているので、上記のようなAlamofireにある便利機能も使えますよ。

他には、API通信を開始する直前やレスポンスを取得した直後などのイベントを取得できるプラグインもあります。プラグインは自分好みにカスタマイズして作成することも可能。

API通信

実際にMoyaProviderを利用してAPIから情報を取得してみます。
下記は「ボタンを押す→API通信→JSONをパース→その結果を返す」処理になります。

通常のMoyaを利用するとResult<Response, Error>型でレスポンスを受け取れますが、下記のようにRxMoyaを利用するとPrimitiveSequence<SingleTrait, Response>型で情報を受け取れます。ResponseはMoyaで定義されているAPIレスポンス用の型で、内部にレスポンスをData型で持っています。

let didFinishGetUsersSubject = PublishSubject<[User]>()

input.tapButton
    .flatMap { self.provider.rx.request(UserAPI.fetch) }
    .flatMap({ (response) -> Observable<[User]> in
        return Observable<[User]>.create({ (observer) -> Disposable in
            do {
                let decoder = JSONDecoder()
                let users = try decoder.decode([User].self, from: response.data)
                observer.onNext(users)
            } catch(let error) {
                observer.onError(error)
            }
            return Disposables.create()
        })
    })
    .bind(to: didFinishGetUsersSubject)
    .disposed(by: disposeBag)

Codableに準拠したクラスにJSONをパースするにはJSONDecoderを利用しましょう。
レスポンスが配列の場合は上記のようにCodableに準拠したクラスをArray型で指定することでデコードできます。

このようにRxMoyaを利用すると、RxSwiftなどでリアクティブな振る舞いを定義している処理の中に、APIの処理をつなぎこみやすくなりますね。

おわりに

最近なんだかモヤモヤしているなという方はMoyaを使ってみてはいかがでしょうか。
github.com