122
95

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Swift4 + Moya + RxSwift + Codableで作るAPIクライアント

Posted at

Moyaとは

MoyaはAlamofireのラッパーで、iOSアプリを作る時に作成されるAPI通信周りの処理をまるっと引き受けてくれるやつです。触ろうと思えばAlamofireを直接さわれるので、Moyaを使って出来ないことは無いです。Alamofireを利用しているならぜひ使いたいライブラリですね。
また、公式にRxSwiftに対応しているため、独自にRxSwift用にレスポンスの変換処理を書く必要がないのも嬉しいです。Moyaに限らずAPI通信周りを担ってくれるほとんどのライブラリはEitherの形でレスポンスが返ってきます。それを自分たちでRxに変換するコードをわざわざ書いているのであれば、Moyaを使って必要な開発にリソースを割いた方がいいです。

Moyaのインストール

CartfileにMoya/Moyaを追加します。

github "Moya/Moya"

その後、carthage bootstrap --platform ios を実行し、Build Phasesにビルド済みのframeworkの追加とRun Scriptの設定を行いましょう。

スクリーンショット 2018-03-02 11.29.16.png

スクリーンショット 2018-03-02 11.24.21.png

RxSwiftと一緒に利用したいのでRxMoya.frameworkRxSwift.frameworkも追加しています。

基本的な使い方

TargetTypeというプロトコルに準拠する形で接続先やパラメータなどを書いていきます。

Api.swift
import Moya

// URL用に文字列をエスケープするためのやつで直接的には関係ありません
extension String {
    var urlEscaped: String {
        return addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
    }
}

enum GitHub {
    case userProfile(String)
}

extension GitHub: TargetType {
    var baseURL: URL { return URL(string: "https://api.github.com")! }
    var path: String {
        switch self {
        case .userProfile(let name):
            return "/users/\(name.urlEscaped)"
        }
    }
    var method: Moya.Method {
        return .get
    }
    var task: Task {
        switch self {
        case .userProfile(_):
            return .requestPlain
        }
    }
    var headers: [String : String]? { return nil }

    // テストの時などに、実際にAPIを叩かずにローカルのjsonファイルを読み込める
    var sampleData: Data {
        let path = Bundle.main.path(forResource: "samples", ofType: "json")!
        return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile()
    }
}
Profile.swift
import Foundation

struct Profile: Codable {
    let login: String
    let url: URL
    let name: String?
    let email: String?
}

参考: https://github.com/Moya/Moya/blob/master/docs/Examples/Basic.md
参考: https://github.com/Moya/Moya/blob/master/docs/Targets.md

呼び出しは以下のように行います。

let disposeBag = DisposeBag()

let provider = MoyaProvider<GitHub>()
provider.rx.request(.userProfile("kouheiszk"))
    .filterSuccessfulStatusCodes()
    .map(Profile.self)
    .subscribe(onSuccess: { (profile) in
        print(profile)
    }) { (error) in
        print(error)
}
.disposed(by: disposeBag)

providerに対してrxを付けてrequestを叩くことでレスポンスをsubscribeできるようになります。
map(Profile.self)のところで、レスポンスのJSONをProfileにマッピングしています。ProfileはCodableに準拠していれば特に何も考える必要がありません。
とても簡単ですね。

応用的な使い方

基本的な使い方で書いた様なAPIのリクエスト先をcaseで記述する方法だと、リクエスト先が増えた際にcaseが増え、TargetTypeが要求するvar pathvar methodなどの中のswitch-caseも増えていきます。2〜3個ならまだ大丈夫ですが、それ以上にcaseが増えると定義が分散して読み辛いコードが出来上がることが容易に想像できます。
caseで分岐しない、API仕様書をそのまま写した様な書き方をしたいという欲求が出てくると思います。

実はMoyaではこのAPI仕様書をそのまま写した様な書き方ができるので、APIのリクエスト先が複数になる場合ははじめからこの書き方にしておいた方がいいです。

caseではなくstructでAPIを定義

新たににTargetTypeに準拠するGitHubApiTargetTypeというprotocolを定義します。baseURLなど各APIで共通の項目はextensionで定義しておきましょう。

protocol GitHubApiTargetType: TargetType {
}

extension GitHubApiTargetType {
    var baseURL: URL { return URL(string: "https://api.github.com")! }
    var headers: [String : String]? { return nil }
    var sampleData: Data {
        let path = Bundle.main.path(forResource: "samples", ofType: "json")!
        return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile()
    }
}

enumのcaseを削除し、GitHubApiTargetTypeに準拠したstructを定義してきます。

enum GitHub {
    struct GetUserProfile: GitHubApiTargetType {
        var method: Method { return .get }
        var path: String { return "/users/\(name.urlEscaped)" }
        var task: Task { return .requestPlain }
        let name: String

        init(name: String) {
            self.name = name
        }
    }
}

APIの定義がまとまり見やすくなりました。
呼び出しはほとんど変わらず、以下のようになります。

let disposeBag = DisposeBag()

let provider = MoyaProvider<GitHub.GetUserProfile>()
provider.rx.request(GitHub.GetUserProfile(name: "kouheiszk"))
    .filterSuccessfulStatusCodes()
    .map(Profile.self)
    .subscribe(onSuccess: { (profile) in
        print(profile)
    }) { (error) in
        print(error)
}
.disposed(by: disposeBag)

共通のProviderを利用する

APIの定義は見やすくなったのですが、今度はproviderを各APIリクエスト毎に都度生成しなくてはいけなくなってしまいました。

GitHub.GetUserRepositories(name: "kouheiszk")

のようなAPIリクエストを行いたいと思った時に、

let provider = MoyaProvider<GitHub.GetUserRepositories>()
provider.rx.request(GitHub.GetUserRepositories(name: "kouheiszk"))

の様に新たにProviderを作る必要があるということです。
理想としては共通のProviderを利用して、さらにはそれすら隠蔽して

Api.request(GitHub.GetUserProfile(name: "kouheiszk"))
Api.request(GitHub.GetUserRepositories(name: "kouheiszk"))

のように、リクエストしたいですよね。

型消去

それぞれの型に対応したProvider。そうです型消去です。型消去をしていきましょう。
AnyGitHubApiTargetTypeを作成し、MoyaProvider<AnyGitHubApiTargetType>()とするイメージです。

しかし早まってはいけません。こういうことはもっと前に考えついている人がいるはずです。
Moyaのドキュメントを見てみるとやはりありました。どうやら以下のようにすれば出来るみたいです。

let provider = MoyaProvider<MultiTarget>()
let target = MultiTarget(GitHub.GetUserRepositories(name: "kouheiszk"))
provider.rx.request(target)

参考:https://github.com/Moya/Moya/blob/master/docs/Examples/MultiTarget.md

これをベースに、理想の呼び出し方を実現させてみましょう。
Codableなレスポンスの型はリクエスト毎に異なるはずなので、GitHubApiTargetTypeにこのレスポンスの型を記述するようにします。

protocol GitHubApiTargetType: TargetType {
    associatedtype Response: Codable
}

struct GetUserProfile: GitHubApiTargetType {
    typealias Response = Profile

    ...
}

リクエストを行うApiクラスは以下のようになります。Providerが開放されると、APIのリクエスト結果をSubscribeできなくなってしまうので、ここではシングルトンでProviderが開放されないようにしています。

class Api {
    static let shared = Api()
    private let provider = MoyaProvider<MultiTarget>()

    func request<R>(_ request: R) -> Single<R.Response> where R: GitHubApiTargetType {
        let target = MultiTarget(request)
        return provider.rx.request(target)
            .filterSuccessfulStatusCodes()
            .map(R.Response.self)
    }
}

呼び出しは以下のようになります。

let disposeBag = DisposeBag()

Api.shared.request(GitHub.GetUserProfile(name: "kouheiszk"))
    .subscribe(onSuccess: { (profile) in
        print(profile)
    }) { (error) in
        print(error)
}
.disposed(by: disposeBag)

とてもシンプルになりました。

完成

最後に作成したApi.swiftの内容を全て貼っておきます。各APIはリクエストの種類毎にApi+User.swiftの様にファイル分割するといいと思います。

//
//  Api.swift
//  MoyaSample
//
//  Created by Kouhei Suzuki on 2018/03/02.
//  Copyright © 2018年 Kouhei Suzuki. All rights reserved.
//

import Moya
import RxSwift

extension String {
    var urlEscaped: String {
        return addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
    }
}

class Api {
    static let shared = Api()
    private let provider = MoyaProvider<MultiTarget>()

    func request<R>(_ request: R) -> Single<R.Response> where R: GitHubApiTargetType {
        let target = MultiTarget(request)
        return provider.rx.request(target)
            .filterSuccessfulStatusCodes()
            .map(R.Response.self)
    }
}

protocol GitHubApiTargetType: TargetType {
    associatedtype Response: Codable
}

extension GitHubApiTargetType {
    var baseURL: URL { return URL(string: "https://api.github.com")! }
    var headers: [String : String]? { return nil }
    var sampleData: Data {
        let path = Bundle.main.path(forResource: "samples", ofType: "json")!
        return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile()
    }
}

enum GitHub {
    struct GetUserProfile: GitHubApiTargetType {
        typealias Response = Profile

        var method: Method { return .get }
        var path: String { return "/users/\(name.urlEscaped)" }
        var task: Task { return .requestPlain }
        let name: String

        init(name: String) {
            self.name = name
        }
    }
}
122
95
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
122
95

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?