Xcode
Swift
Alamofire
RxSwift
Codable

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

前置き

担当プロジェクトで、ようやく
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"
github "ReactiveX/RxSwift"

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 } // ヘッダー情報

}

extension BaseAPIProtocol {

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

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

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

など場合分けして、それぞれが基底プロトコルに準じたものを作成していくと、コード分割ができ綺麗に作っていくことができるかと思います。
今回は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)
}

// MARK: - ErrorResponse
// 自分でデコードのエラーだとわかるならなんでもいいです。
struct ErrorResponse: Error, CustomStringConvertible {
    let description: String = "-- Decode Error --"
    var dataContents: String?
}

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

// MARK: - JSONDecoder Extension
extension JSONDecoder {

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

}

②通信部分の作成

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

import UIKit
import Alamofire

// MARK: - API Manager
struct APIManager {

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


    // MARK: Static Methods

    // 実際に呼び出すのはこれだけ。
    public 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)
                .subscribe(onSuccess: { onSuccess($0) },
                           onError: { onError($0) })
                .disposed(by: disposeBag)
    }

    // RxSwiftを導入している部分。成功/失敗いずれかしか返らないSingleにしてある。
    private 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 customAlamofire(request).responseJSON { response in
                switch response.result {
                case .success: completion(decodeData(request, response.data))
                case .failure(let error): completion(.failure(error)
                }
            }
    }

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

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

    // JSONをDecoderしている部分
    private static func decodeData<T, V>(_ request: T, _ data: Data?) -> APIResult
        where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {

            if let d = data, let result = try? JSONDecoder(type: .convertFromSnakeCase).decode(V.self, from: d) {
                return .success(result)

            } else { // Decodeエラー時はErrorResponseを返すようにしている。またdata内容も付与しておく。
                return .failure(ErrorResponse(dataContents: String(data: data ?? Data(), encoding: .utf8)))

            }

    }

}


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

使用例

let disposeBag = DisposeBag()

APIManager.call(xxxRequest.get, disposeBag, onNext: {
  // do some thing

}, onError: {
  // error handling

})

たったのこれだけです!

あとがき

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

PS

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