LoginSignup
65

More than 1 year has passed since last update.

[Swift]Alamofire+RxSwift+CodableでAPIクライアントを作る

Last updated at Posted at 2018-04-15

2019/8/13 続編を書きました
URLSession+Combine+CodableでAPIクライアントを作る

2020/1/28 続編を書きました
Alamofire+RxSwiftでUpload APIクライアントを作る

追記
Decode部分をprotocolに入れることでメソッドを減らしました。
(プロトコルなので、継承先で柔軟に上書けるようになった)

前置き

担当プロジェクトで、ようやく
Objective-c+Swift3.2 → Swift4.0

への書き換えも済み、それならCodableを使いたい!と思い行ってみた。

半年前に ObjectMapper+RxSwiftを実装した備忘録 書いたが、これをCodableに対応したものにした。
(基本的には上記内容と同じです。Decode部分が若干かわったくらい)

導入にあたって

ちょろちょろこういうの
- Swift4 + Moya + RxSwift + Codableで作るAPIクライアント
- Moya+RxSwift+CodableでAPI通信

はあったんですが、なんかあんまり綺麗じゃないと思ってしまい、、(ディスリじゃないですよ!)

あとMoyaのメリットが、実感できなかったのでAlamofireでやりました。

導入

実際に、プロジェクトに導入する手順で行っていく。
通信にはAlamofireを使用します。

1. ライブラリのインストール

ライブラリの導入はCarthageを使用。
Cocoapods,SPMなど導入できるのであれば何でもおk)

github "Alamofire/Alamofire" ~> 4.1
github "ReactiveX/RxSwift" == 4.3.1

Linked Frameworks and Libraryにもライブラリ追加を忘れずに。

2. ネットワークプロトコルの作成

この記事ではただコードを載せただけだったので、若干細かく説明していきたいと思います。

①定義ファイルの作成

APIで使用するURLやPathを一元管理するためにenumで定義し管理していきます。
(このやり方が好きでない人は、リクエストごとに直にpathを書いていってください)

今回は例としてsignIn, signUpというものを定義しています。

// MARK: - Constants
enum APIConstants {
    case signIn
    case signUp
    case xxx
        ,
        ,

    // MARK: Public Variables
    public var path: String {
        switch self {
        case .signIn: return "user_sign_in"
        case .signUp: return "user_sign_up"
        case .xxx: return "xxx"
                ,
                ,
        }
    }


    // MARK: Public Static Variables
    public static var baseURL = "https://~~~"

    public static var header: HTTPHeaders? {
        // 必要ならば個々に設定してください
        return [
            "Accept-Encoding": ~~~, 
            "Accept-Language": ~~~,
            "User-Agent": "~~~~"
        ]
    }
}

②リクエストの基底プロトコルを作成

requestを作成するための雛形protocolを作成します。
Alamofireを使用するので、それに準拠するように作っていきます。

import Alamofire

// MARK: - Base API Protocol
protocol BaseAPIProtocol {
    associatedtype ResponseType // レスポンスの型

    var method: HTTPMethod { get } // get,post,delete などなど
    var baseURL: URL { get } // APIの共通呼び出し元。 ex "https://~~~"
    var path: String { get } // リクエストごとのパス
    var headers: HTTPHeaders? { get } // ヘッダー情報
    var decode: (Data) throws -> ResponseType { get } // デコードの仕方
}

extension BaseAPIProtocol {

    var baseURL: URL { // 先ほど上で定義したもの。
        // 絶対にあることがある保証されているので「try!」を使用している
        return try! APIConstants.baseURL.asURL() 
    }

    var headers: HTTPHeaders? { // 先ほど上で定義したもの。なければ「return nil」でok
        return APIConstants.header 
    }

    var decode: (Data) throws -> ResponseType { // decoderはあとで用意するので注意
        return { try JSONDecoder.decoder.decode(ResponseType.self, from: $0) }
    }
}

この基底プロトコルを元に
- データを取得する
- アップロードする
- ダウンロードする

など場合分けして、それぞれが基底プロトコルに準じたものを作成していくと、コード分割ができ綺麗に作っていくことができるかと思います。
今回はAPIを叩いてデータを取ってくるだけの想定で話を進めます。以下、普通の通信を行う場合のプロトコル。

Alamofireに準ずるためURLRequestConvertibleを継承しています。
(リクエストするときにURLRequestConvertibleを継承したものを渡す必要があるので)

// MARK: - BaseRequestProtocol
protocol BaseRequestProtocol: BaseAPIProtocol, URLRequestConvertible {
    var parameters: Parameters? { get }
    var encoding: URLEncoding { get }
}

extension BaseRequestProtocol {
    var encoding: URLEncoding {
        // parameter の変換の仕方を設定 
        // defaultの場合、get→quertString、post→httpBodyとよしなに行ってくれる
        return URLEncoding.default
    }

    func asURLRequest() throws -> URLRequest {
        // requestごとの pathを設定
        var urlRequest = URLRequest(url: baseURL.appendingPathComponent(path))

        // requestごとの methodを設定(get/post/delete etc...)
        urlRequest.httpMethod = method.rawValue

        // headersを設定
        urlRequest.allHTTPHeaderFields = headers

        // timeout時間を設定
        urlRequest.timeoutInterval = TimeInterval(30)

        // requestごとの parameterを設定
        if let params = parameters {
            urlRequest = try encoding.encode(urlRequest, with: params)
        }

        return urlRequest
    }

}

ここまではこの記事とほぼ同じです。

3.リクエスト・レスポンスの作成

基底プロトコルを作ったので、実際にそれに適応するリクエストとレスポンスを作成していきます。

例を用意。APIを叩くと以下のレスポンス(Json)が返ってくるとする。(あえて階層構造なものにしました)

{
    "data": [
        {
            "title": "タイトル", 
            "screen_name": "トップ", 
            "id": "00000", 
        },
        {
            "title": "タイトル", 
            "screen_name": "トップ", 
            "id": "00000", 
        },
            ,
            ,
    ], 
    "result": true
}

①レスポンスの作成

上記のJsonを対応させていきたいと思います。
またCodableに準拠させます。

import Alamofire

// MARK: - Response
struct xxxResponse: Codable {
    var data: [xxxViewModel]?
    var result: Bool = false

    // 特に型変換などがない場合は、init省略可なので省略
}

// MARK: - Model
struct xxxViewModel: Codable {

    let title: String?
    let screenName: String?
    let id: Int?

    enum Keys: String, CodingKey {
        case title
        case screenName
        case id
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Keys.self)

        title = try container.decode(String.self, forKey: .title)
        screenName = try container.decode(String.self, forKey: .screenName)
        id = Int(try container.decode(String.self, forKey: .id))
    }
}

②リクエストの作成

先ほど作成したBaseRequestProtocolに準拠させます。
今回はgetの場合を例に。

import Alamofire

// MARK: - Request
enum xxxRequest: BaseRequestProtocol {
    typealias ResponseType = xxxResponse

    case get

    var method: HTTPMethod {
        switch self {
        case .get: return .get
        }
    }

    var path: String { // 先ほど定enumで定義したもの
        return APIConstants.xxx.path
    }

    var parameters: Parameters? { // サーバーになにか送る必要がある場合は、こちらでセットする
        return nil
    }

}

ここまでで、下準備は完成です。

4.ネットワークマネージャーを作成

①Resultタイプの作成

通信成功時はCodableに準拠したものが返ってくるように設定します。

// MARK: - ResultType
enum APIResult {
    case success(Codable)
    case failure(Error)
}

Swift4.1からCodableのキーに関して、 SnakeCace ⇆ CamelCaceの変換がデフォルトでやりやすくなったので、Extensionで拡張してデフォルトで付与しています。

また、それ用のデコーダーを先に定義しておきます。
(使用先でハードコードしていただいても問題ないです。個人的には記述が長くなるのが好きではないだけなので)

// MARK: - JSONDecoder Extension
extension JSONDecoder {

    convenience init(type: JSONDecoder.KeyDecodingStrategy) {
        self.init()
        self.keyDecodingStrategy = type
    }

    static let decoder: JSONDecoder = JSONDecoder(type: .convertFromSnakeCase)
}

②通信部分の作成

強制キャストしているところがありますが、それは必ずなることが担保されているので行なっています。
(自分は基本的には強制キャストしません。ただ不必要なnilチェックでコードが汚くなるのは嫌いなので)

import UIKit
import Alamofire

// MARK: - API Cliant
struct APICliant {

    // MARK: Private Static Variables
    private static let successRange = 200..<400
    private static let contentType = ["application/json"]


    // MARK: Static Methods

    // 実際に呼び出すのはこれだけ。(rxを隠蔽化しているだけなので、observeでも大丈夫)
    static func call<T, V>(_ request: T, _ disposeBag: DisposeBag, onSuccess: @escaping (V) -> Void, onError: @escaping (Error) -> Void)
        where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {

            _ = observe(request)
                .observeOn(MainScheduler.instance) // mainを指定してしまっているので消しても良い
                .subscribe(onSuccess: { onSuccess($0) },
                           onError: { onError($0) })
                .disposed(by: disposeBag)
    }

    // RxSwiftを導入している部分。成功/失敗いずれかしか返らないSingleにしてある。
    static func observe<T, V>(_ request: T) -> Single<V>
        where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {

            return Single<V>.create { observer in
                let calling = callForData(request) { response in
                    switch response {
                    //※ 既にsuccessしているので「as! V」で強制キャストしている(できる)
                    case .success(let result): observer(.success(result as! V))
                    case .failure(let error): observer(.error(error))
                    }
                }

                return Disposables.create() { calling.cancel() }
            }
    }

    // Alamofire呼び出し部分
    private static func callForData<T, V>(_ request: T, completion: @escaping (APIResult) -> Void) -> DataRequest
        where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {

            return request(request).responseJSON { response in
                switch response.result.flatMap(request.decode) {
                case .success(let data): completion(.success(data))
                case .failure(let error): completion(.failure(error))
                }
            }
    }

    // Alamofireのメソッドのみ切り出した部分
    private static func request<T>(_ request: T) -> DataRequest
        where T: BaseRequestProtocol {

            return Alamofire
                .request(request)
                .validate(statusCode: successRange)
                .validate(contentType: contentType)
    }
}


ちょっといきなりかなりの量だったかもしれませんが、、、笑
使用する時はかなり綺麗にかけます

使用例

let disposeBag = DisposeBag()

APICliant.call(xxxRequest.get, disposeBag, onSuccess: {
  // do some thing

}, onError: {
  // error handling

})

たったのこれだけです!

自前でdo, mapでレスポンスを加工したり、observeOnなどでQueueを変えたい場合は、observeの方を呼び出すと良いでしょう。

let disposeBag = DisposeBag()

APICliant.observe(request)
    .observeOn(MainScheduler.instance)
    .subscribe(onSuccess: { response in
        print("onSuccess")
    }, onError: { error in
        print("onError")
    })
    .disposed(by: disposeBag)

複数APIの呼び出し

Observableに変換してcombineLatestでくっつけることで実現可能です。

以下に例を用意しておきます。(型は存在しないものを使用しているので、各自のものにしてください)

let disposeBag = DisposeBag()
let relay = PublishRelay<Void>() 

func call(onNext: @escaping (XXXYYY) -> Void) {
    // API 1つ目
    let xxxObservable: Observable<XXX> = relay.flatMapLatest { [weak self] _ in
        let request = XXXRequest()     
        return APICliant.observe(request)
    }

    // API 2つ目
    let yyyObservable: Observable<YYY> = relay.flatMapLatest { [weak self] _ in
        let request = YYYRequest()     
        return APICliant.observe(request)
    }

    Observable
        .combineLatest(xxxObservable, yyyObservable) { 
            // 2つのデータを加工する処理('XXXYYY'の型になるように変換)
        }
        .subscribe(onNext: onNext)
        .disposed(by: disposeBag)

    relay.accept(())
}

リクエストに引数が必要な場合は、callする関数なりに追加して渡してください。
(また、今回は成功時の処理しかやっていませんが、必要に応じてエラー処理も各自追加してください)

あとがき

個人的にはまだObject Mapperの方が便利かなぁと思ってしまいます笑
だいぶCodableが流行ってきており、使い勝手もよくなってきたような気がしています。

型変換が不便なのと、Codable準拠のレスポンス変数に対してdidSetが使えないのが個人的には辛いですね。
(→レスポンス受け取った瞬間にdidSetで何かすることができなくなった(didSetが呼ばれない)ので)

PS

プロジェクトのクラス名をまんま使用するわけにいかず、一部リネームして表記しているので動かなかったらすいません、、、
というより、誤字脱字あればお願いいたします笑

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
65