LoginSignup
12

More than 5 years have passed since last update.

SwiftでAPIリクエストを発行する。

Posted at

はじめに

以前[Swift] RxSwift + Alamofire + ObjectMapper + Realmの基本的な使い方と言う記事を書いたのですが、一年経った今ではそのまま使えない上に理解に時間がかかりました。
なので、再度API通信をする方法について考えてみました。

以前の方法ではRouter部分と呼び出される処理の部分が離れているため、APIを追加するために3箇所にコードを書く必要がありました。
これにモデルを入れると以下の4箇所になってしまいます。

  • Router部分
  • API部分
  • API呼び出し部分
  • モデル

これをもう少し端折りたくて、今回は一部の整合性チェックを捨てて簡易的に実装できるものを作ってみました。
この方法が良いかどうかは・・・わかりません。

使っているライブラリー

使用しているライブラリーは以下です。

github "ReactiveX/RxSwift"
github "Alamofire/Alamofire" ~> 4.4
github "Hearst-DD/ObjectMapper" ~> 2.2
github "tristanhimmelman/AlamofireObjectMapper" ~> 4.0

僕はcarthageで入れましたがCocoaPodsで入れても問題ありません。
Realmとも組み合わせれますが、このコードを汎用的に使いたかったのでRealmへの保存は業務コードに委託してます。(すなわち、この記事では取り扱ってないです。)

コード

はじめにコードと使い方を書きます。
一部変更が必要(後述)ですが、コピペすれば動きます。

API.swift
import UIKit
import Alamofire
import RxSwift
import ObjectMapper
import AlamofireObjectMapper

// Routerを実装してAPIを作成ます。
public protocol Router {
    // URLを返却 
    func url() -> String
    // メソッドを返却
    func method() -> Alamofire.HTTPMethod
    // 実際のデータ取得処理
    func request<T: Mappable>() -> Observable<[T]>
    func request<T: Mappable>() -> Observable<T>
    func request<T: Mappable>(parameters: Parameters?) -> Observable<[T]>
    func request<T: Mappable>(parameters: Parameters?) -> Observable<T>
    // エラー時の処理
    func errorHandle(error: Error, statusCode: Int?)
}

// Routerの共通処理をあらかじめ定義しておく。
extension Router {
    // Defaultはget処理
    func method() -> Alamofire.HTTPMethod { return .get }

    // 実際のリクエスト発行
    func request<T: Mappable>() -> Observable<[T]> {
        return self.request(parameters: nil)
    }
    func request<T: Mappable>(parameters: Parameters? = nil) -> Observable<[T]> {
        return Observable.create { (observable: AnyObserver<[T]>) in
            // リクエストを投げる。
            API.createRequest(router: self, parameters: parameters).responseArray { (response: DataResponse<[T]>) in
                switch response.result {
                case .success(let value):
                    observable.on(.next(value))
                    observable.onCompleted()
                case .failure(let error):
                    self.errorHandle(error: error, statusCode: response.response?.statusCode)
                }
            }
            return Disposables.create()
        }
    }
    func request<T: Mappable>() -> Observable<T> {
        return self.request(parameters: nil)
    }
    func request<T: Mappable>(parameters: Parameters? = nil) -> Observable<T> {
        return Observable.create { (observable: AnyObserver<T>) in
            // リクエストを投げる。
            API.createRequest(router: self, parameters: parameters).responseObject { (response: DataResponse<T>) in
                switch response.result {
                case .success(let value):
                    observable.on(.next(value))
                    observable.onCompleted()
                case .failure(let error):
                    observable.onError(error)
                    self.errorHandle(error: error, statusCode: response.response?.statusCode)
                }
            }
            return Disposables.create()
        }
    }

    // エラー処理
    func errorHandle(error: Error, statusCode: Int?) {
        if statusCode == 401 {
            // 共通エラー処理をここに記載します。
        }
    }
}

// API処理
class API {
    // ホスト名
    private static let Host = "http://localhost:3000/api/v1"

    // 共通ヘッダー
    static let CommonHeaders:HTTPHeaders = [
        "Authorization": "Test",
        "Version": Bundle.main.infoDictionary!["CFBundleShortVersionString"]! as! String,
        "Accept": "application/json"
    ]

    // リクエスト処理の生成
    fileprivate class func createRequest(router:Router, parameters: Parameters? = nil) -> Alamofire.DataRequest {
        return Alamofire.request("\(Host)\(router.url())",
            method:router.method(),
            parameters: parameters,
            encoding: JSONEncoding.default,
            headers: API.CommonHeaders).validate()
    }
}

使い方

モデル

使用するモデルを作ります。
定番ですがUserモデルとします。

User.swift
import Foundation
import ObjectMapper

class User: Mappable {
    var id = 0
    var name = ""

    required init?(map: Map) {}

    // Mappable
    func mapping(map: Map) {
        id    <- map["id"]
        name <- map["name"]
    }
}

ObjectMapperを使うことで、返却されるJsonをマッピングさせます。
想定されるJsonは以下の通りです。

{
"id": 1,
"name": "hoge"
},
{
"id": 2,
"name": "foo"
}

※keyPathについては未考慮(後述)

ルーティング

ルーティング用のクラスは以下の通りです。

Users.swift
extension API {
    enum Users: Router {
        // データ一覧取得
        case list
        // データ一件取得
        case get(Int)

        // ルーティングはここで行います。
        func url() -> String {
            switch self {
            case .list:
                return "/users"
            case .get(let id):
                return "/users/\(id)"
            }
        }

        // methodも上記同様に定義できます。未定義の場合はgetが使われます。
    }
}

今回のケースではhttp://localhost:3000/api/v1を共通URLとして定義しているので、以下のURLにアクセスされます。

呼び出し方

実際に呼び出す時には以下のように呼び出します。

UsersViewController.swift
class UsersViewController: UIViewController {
    // APIの解放で使います。わからない人はこのまま実装すればOKです。
    let disposeBag = DisposeBag()

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        // 一覧取得
        // パラメータをつけたい場合はparametersに[String:String]で定義。なくても動きます。
        API.Users.list.request(parameters: nil).subscribe(onNext: { (users:[User]) in
            for user in users {
                DebugLog("id = \(user.id)")
            }
        }).addDisposableTo(disposeBag)

        // 一件のデータ取得
        API.Users.get(1).request().subscribe(onNext: { (user:User) in
            DebugLog("id = \(user.id)")
        }).addDisposableTo(disposeBag)
    }
}

解説

肝となっているのはAPIクラスです。
こちらのクラスでAlamofireのリクエストを生成してます。
共通ヘッダーなどを定義する場合はこのクラスに行います。

Routerプロトコルはやや強引に作っています。
冒頭に書いた整合性が取れない原因がT: Mappableの部分になります。
本来ならUserオブジェクトで受け取る必要がある箇所で、Companyを定義してしまっても落ちることなく処理が進んでいきます。

        API.Users.get(1).request().subscribe(onNext: { (user:Company) in
            DebugLog("id = \(user.id)")
        }).addDisposableTo(disposeBag)

上記のようにクラスを誤ってもコンパイルエラーは発生しません。
Routerを隠蔽して、別のクラスを作成すればUser意外は許可しないと言うこともできるのですが、今回はこの点を見送りました。

そして、共通処理を全てextension Routerに委ねています。
もしkeyPathを設定して

"users": [
  {
  "id": 1,
  "name": "hoge"
  },
  {
  "id": 2,
  "name": "foo"
  }
]

このようなデータを取得した場合はこちらの処理を変更します。

API.createRequest(router: self, parameters: parameters)
    .responseArray { (keyPath: "users", response: DataResponse<[T]>)

あとはRxSwiftを使って呼び出し側へ処理を返します。

return Observable.create { (observable: AnyObserver<T>) in
    ...
    return Disposables.create()
}

※NopDisposableが無くなっていて焦った・・・。

最後に

このコードはまだ出来たばかりで実戦投入はしていません。
なので、いろいろ欠点があるかもしれませんが基本的な考え方はきっとあっているはずです(苦笑

1年前に作った時と色々変わっていたので、この記事も1年後に使えるかどうかは微妙です。
ただ、前の記事もまったく参考にならなかったわけではなく前の記事を参考にしたから1日で実装出来たわけなので来年になっても多少は使えるだろうなぁと思ってます。

本当はAlamofireObjectMapperは使わないで実装しようと思ったのですが、うまくAlamofireからObjectMapperに落とせなかったので使ってしまいました。(ループ処理を書きたくなかった・・・。)
それに使ってみるとこっちの方が便利だなーと思ったので結果OKと言う結論に至りました。

僕の調べ方が悪いのか、Swiftの日本語文献はあまりないので誰か(1年後の僕)の役に立てればいいな。
では。

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
12