LoginSignup
51
36

More than 3 years have passed since last update.

iOSDC Japan 2019公式アプリで採用しているAPI通信周りの実装

Last updated at Posted at 2019-09-11

こんにちは。iOSエンジニアの@h1d3mun3です。

皆さん、今年9月5日から9月7日まで、早稲田大学で開催された「iOSDC Japan 2019」にはご来場いただけましたでしょうか。

エンジニアが主役のお祭りとして、毎年多くの方にご来場いただいております。

iOSDC Japan で公式アプリを出させて頂く機会に恵まれました。

DC Japan

無事リリースすることもでき、Opt-In Onlyながら、会期期間中のクラッシュが0件を達成することができ、大変良かったと思っております。

アプリで一番難儀する場所といえば多分API通信周りなのではないでしょうか。

ここでは、iOSDC Japan 2019公式アプリで採用したAPI通信周りの知見を共有することにより、将来API通信周りの実装で悩む人の一助となることを期待しています。

1. はじめに - iOSDC Japan公式アプリのアーキテクチャ

iOSDC Japan公式アプリ (以下本アプリ)では、Kickstarter式のMVVMアーキテクチャを採用しています。

1つの画面を

  • ViewController
  • ViewModel (Inputs / Outputsで切り分け)

の2つのファイルで構成しています。

  • ViewModelOutputsObservable を公開している
  • ViewController 側で OutPutssubscribe する
  • ViewControllerViewModelinputs の処理を呼ぶ 、というイメージです。

いわゆる 「ビジネスロジック」はすべて ViewModel 側に持たせており、 ViewController は表示を行うだけ、という状態です。

非同期の処理が主となるため、実装には RxSwift を採用しています。

Kickstarter式のMVVMアーキテクチャについては下記記事によくまとまっております。
(僕もこれを読んで勉強いたしました)

Kickstarter-iOSのViewModelの作り方がウマかった

また、構造体の場合イニシャライザを書かなくても全項目イニシャライザが自動で用意されるので楽という理由で、クラスよりも構造体を多用する設計にしています。

2. API通信周りの実装

ここでは、タイムテーブルを取得する処理を例に上げ、本アプリがどのような仕組みで通信を行っているかを説明します。

2.1 HTTP通信を担当する構造体

本アプリでは下記のような構造体を宣言し、HTTP通信を担当させています。
GETPOST しかないのは単純にそれ以外が必要なかったからです。必要に応じて追加します。

import Foundation
import RxSwift

protocol HttpProtocol {
    func get(url: String, param: [String: Any]?) -> Single<(response: HTTPURLResponse, data: Data)>
    func post(url: String, param: [String: Any]?) -> Single<(response: HTTPURLResponse, data: Data)>
}

struct Http {
}

extension Http: HttpProtocol {
    func get(url: String, param: [String: Any]?) -> Single<(response: HTTPURLResponse, data: Data)> {
        var urlComponents = URLComponents(string: url)!
        urlComponents.queryItems = param?.map { URLQueryItem(name: $0, value: "\($1)") }
        var request = URLRequest(url: urlComponents.url!)
        request.timeoutInterval = TimeInterval(30)
        request.httpMethod = "GET"
        return URLSession.shared.rx
            .response(request: request)
            .asSingle()
    }

    func post(url: String, param: [String: Any]?) -> Single<(response: HTTPURLResponse, data: Data)> {
        var request = URLRequest(url: URL(string: url)!)
        request.timeoutInterval = TimeInterval(30)
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "POST"
        if let param = param {
            request.httpBody = param.percentEscaped().data(using: .utf8)
        }
        return URLSession.shared.rx
            .response(request: request)
            .asSingle()
    }
}

作りとしてはとても単純で、

  1. HttpProtocol により、振る舞いを制限 (これよりテスタブルになる)
  2. HttpProtocol を適合した構造体をつくって実際の通信をそこに埋め込む

というだけです。

通信の結果は Single で帰ってくるようになっており、非同期処理をするのに便利な作りにしてあります。

(Singleにするので必ず結果が帰ってくる or エラーするのどちらかにしかならないので便利)

2.2 実際のAPI通信コード

実際にタイムテーブルを取得してくる構造体は下記のようなコードになります。

具体的な APIエンドポイント などは、APIConstants に集約しております。

import Foundation
import RxSwift

struct TimetableRequestApi {
    let http: HttpProtocol

    func call() -> Single<TimetableAPIResponse> {
        return http.get(url: APIConstants.timetable.url, param: nil)
            .map { response -> TimetableAPIResponse in
                if response.response.statusCode != 200 { throw DCError.internalServerError }
                let decoder: JSONDecoder = JSONDecoder()
                decoder.keyDecodingStrategy = .convertFromSnakeCase
                decoder.dateDecodingStrategy = .iso8601
                do {
                    let response = try decoder.decode(TimetableAPIResponse.self, from: response.data)
                    return response
                } catch {
                    throw DCError.jsonParseError
                }
        }
    }
}

APIからのレスポンスのマッピングには Decodable を利用しています。

このAPIでは、 TimetableAPIResponse という Decodable に準拠したモデルにデコードしています。

2.3 実際の利用ケース

実際の利用では下記のように利用します。

let request = TimetableRequestApi(http: Http())

request.call()
    .subscribe(onSuccess: { [weak self] result in
        guard let self = self else { return }
        // DO SOMETHING WHAT YOU WANT
        }, onError: { error in
        // エラー処理とか
    })
    .disposed(by: disposeBag)

2.4 補足 - リクエストにidなどが必要な場合

リクエストに当たりパラメータが必要な場合(たとえばある特定のメンバーの情報が欲しいのでリクエストの際にメンバーIDが必要、などの場合)は、 RequestAPI クラスに情報をもたせる作りにしてあります。

3. おわりに

API通信周りは基本的にどのアプリでも実装する(はず)のものなので、皆様苦心されるところだと思います。

僕も苦心しておりましたが、本アプリで実装した形が ひとまず 僕にとってスッキリする実装でしたのでQiitaにで共有させていただきました。

こういう形があるよ!とかあると思うので、皆さま是非是非参考にしてAPI周りをきれいに実装しちゃってくださいませ〜!

51
36
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
51
36