0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Swift]APIKit + MVVM + RxSwiftでデータ取得をしてみる

Last updated at Posted at 2025-09-02

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
      • データを削除する
  • dataParserでAPIKitがレスポンスを処理するときに、JSONとして解釈できるようにする
  • responseでJSONか確認し、Response型に変換
GourmetRequest
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を受け取るための「型の枠組み」を作成してます。

GourmetResponse
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に流すことができるようになります。

APIClientProtocol
import RxSwift
import APIKit

protocol APIClientProtocol {
    func send<T: Request>(_ request: T) -> Single<T.Response>
}
APIClient
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] を返す」ことが保証します。

GourmetRepository
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)
→ 並んだ商品(ラーメン屋一覧)を見て楽しむ

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?