APIKitを利用してAPIデータを取得する実装
今回はAPIKitライブラリを利用しホットペッパーAPIデータを表示させる実装を行います。
APIKitは、iOS/macOSアプリ開発で使われるSwift用のHTTP通信(APIのリクエスト・レスポンス処理)を簡単に行なってくれるライブラリです。
アプリがサーバーとやり取りする際の通信処理「データを送る・データをください」などの依頼を簡単・安全に書けるようにしてくれるツールです。
APIKitのメリット
-
開発が速くなる
- 定型的なHTTP通信のコードを自動生成してくれるので、開発スピードが格段に上がる
-
型安全でエラーが少ない
- JSON → Swiftの型変換を自動で行うため、間違った型のデータを扱う心配が減る
- 例:ユーザーの年齢が必ず Int で来ることが保証される
-
コードが読みやすい & シンプル
- URLSessionやJSONDecoderを毎回書く必要がなく、リクエスト内容を定義するだけで通信できる
-
再利用・メンテナンスがしやすい
- 一度作ったリクエストや共通処理を何度でも使える。API仕様が変わっても修正箇所が少なくて済む
APIKitのデメリット
-
学習コストがある
- Swiftのプロトコルやジェネリクスの理解が必要で、最初は少し難しい
-
柔軟性がやや低い
- 型に沿ったレスポンスを前提にしているため、API仕様が突然変わると型エラーやクラッシュの可能性がある
-
小規模アプリには過剰
- 少量の通信しかない場合、導入するコストがメリットを上回ることもある
参考文献
APIの取得手続き
APIの取得手続きについては、こちらの記事を参考にして進めます。
APIKitライブラリの追加
podfileにライブラリを追加し、pod install
or pod update
を実行
pod 'APIKit'
pod 'RxSwift'
pod 'RxCocoa'
実装コード
リクエスト定義:GourmetRequest
APIKit の Request プロトコルを準拠させます。
- APIキーのハードコード(直書き)を避けるためInfo.plistから取得
- リクエスト先のbaseURLとpathを指定
- それぞれの責務は?
- baseURL → サーバーの場所。例えると「都道府県・市区町村」
- path → サーバー内のどの機能を呼ぶか。例えると「番地・建物名」
- queryParameters → 検索条件やオプション。例えると「部屋番号や呼び出し先」
- それぞれの責務は?
- HTTPリクエストの種類(HTTPメソッド)の指定
- GET
- データを取得する(例: 店舗一覧を取る)
- POST
- データを送信・登録する(例: 新しいユーザーを登録)
- PUT
- 既存データを置き換える(例: プロフィールを更新)
- DELETE
- データを削除する
- GET
- dataParserでAPIKitがレスポンスを処理するときに、JSONとして解釈できるようにする
- responseでJSONか確認し、Response型に変換
import Foundation
import APIKit
struct GourmetRequest: Request {
typealias Response = GourmetResponse
private var key: String {
guard let apiKey = Bundle.main.object(forInfoDictionaryKey: "GourmetAPIKey") as? String else {
fatalError("Missing GourmetAPIKey in Info.plist")
}
return apiKey
}
private let keyword: String
var baseURL: URL {
guard let url = URL(string: "https://webservice.recruit.co.jp") else {
fatalError("ファイルが見つかりません")
}
return url
}
var method: HTTPMethod = .get
var path: String {
return "/hotpepper/gourmet/v1/"
}
var queryParameters: [String: Any]? {
return [
"key": key, // APIキー
"keyword": keyword, // 検索ワードなど
"format": "json" // レスポンス形式をJSONに設定
]
}
// JSONDataParser の readingOptions を指定
var dataParser: DataParser {
return JSONDataParser(readingOptions: [])
}
init(keyword: String) {
self.keyword = keyword
}
func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
// object が Dictionary であることを確認
guard let jsonObject = object as? [String: Any] else {
print("Unexpected object type:", type(of: object))
throw ResponseError.unexpectedObject(object)
}
do {
// Dictionary を Data に変換
let data = try JSONSerialization.data(withJSONObject: jsonObject)
// デコードのためにJSONDecoderを使用
let decoder = JSONDecoder()
// データをデコード
return try decoder.decode(Response.self, from: data)
} catch {
throw ResponseError.unexpectedObject(object)
}
}
}
レスポンスモデル: GourmetResponse
ここではAPIレスポンスのJSONを受け取るための「型の枠組み」を作成してます。
struct GourmetResponse: Codable {
let results: Results
}
// MARK: - Results
struct Results: Codable {
let shops: [Shop]
enum CodingKeys: String, CodingKey {
case shops = "shop"
}
}
APIクライアント:APIClientProtocol
ここでサーバーと通信を行い、APIKitで送ったリクエストの結果を、RxSwiftのSingleとして扱えるようにしている。
RxSwiftのSingleとして扱うことで、レスポンス結果を自由に加工してUIに流すことができるようになります。
import RxSwift
import APIKit
protocol APIClientProtocol {
func send<T: Request>(_ request: T) -> Single<T.Response>
}
import Foundation
import RxSwift
import APIKit
final class APIClient: APIClientProtocol {
func send<T: Request>(_ request: T) -> Single<T.Response> {
return Single.create { single in
let task = Session.send(request) { result in
switch result {
case .success(let response):
single(.success(response))
case .failure(let error):
single(.failure(error))
}
}
return Disposables.create {
task?.cancel()
}
}
}
}
リポジトリ:GourmetRepository
ここでは「キーワードで店舗を検索すると [Shop] を返す」ことが保証します。
protocol GourmetRepositoryProtocol {
func searchGourmet(keyword: String) -> Single<[Shop]>
}
// 店舗APIデータ呼び出し
final class GourmetRepository: GourmetRepositoryProtocol {
private let apiClient: APIClientProtocol
init(apiClient: APIClientProtocol = APIClient()) {
self.apiClient = apiClient
}
func searchGourmet(keyword: String) -> Single<[Shop]> {
let request = GourmetRequest(keyword: keyword)
// APIKitでリクエストを実行
return apiClient.send(request).map { $0.results.shops }
}
}
後の流れを直感イメージとしてまとめると
①お客さん(UI)
→ 「ラーメン屋の一覧が欲しい」とリクエスト
②棚に並べる人(ViewModel)
→ 倉庫に依頼するためRepositoryへお願いする
③商品を仕分けする人(Repository)
→ 注文票(GourmetRequest)を書いて、倉庫の人(APIClient)に渡す
④倉庫の人(APIClient)
→ 注文票を倉庫に渡して、段ボール(JSONレスポンス)を持ってくる
⑤段ボールのラベル(GourmetResponse)
→ 「中身は results.shops です」と保証する
⑥商品を仕分ける人(Repository)
→ ラベルを見て、中身の [Shop] だけ取り出す
⑦棚に並べる人(ViewModel)
→ [Shop] を加工して、UIへ渡しやすい形にする
⑧お客さん(UI)
→ 並んだ商品(ラーメン屋一覧)を見て楽しむ