[Swift]RxSwift×MVVM　ホットペッパーAPIを利用したアプリを作ってみた　Part 1

# APIを利用した実装
引続きRxSwiftの実装をアウトプットしてます。
今回はホットペッパーAPIを利用してテキスト入力からキーワード検索を行い、情報をTableViewに表示させる実装を行いました。

# 実装コード
実装したコードをファイル毎に区切り説明をしていきます。

## ホットペッパーAPIを取得する手続き
ホットペッパーAPIを取得するためのURLは下記になり取得手順を記載します。
https://webservice.recruit.co.jp/doc/hotpepper/reference.html

1. 遷移先の画面中腹にある新規登録からメール登録を行い、返信メールから承認URLをクリックします
2. 承認すると再度メールが届きますので、そこにAPIキーが記載されてますので利用します

## GourmetResponseファイル

まずはModel側の実装です。ここでは実際に取得しView側で表示する必要のあるデータを定義していきます。今回であれば店名・住所・ジャンル・予算・収容人数・営業時間・URL・画像を取得したかったのでCodableで定義してます。

```Swift:GourmetResponse

import Foundation

struct GourmetResponse: Codable {
  let results: Results
}

// MARK: - Results
struct Results: Codable {
  let shops: [Shop]
   
  enum CodingKeys: String, CodingKey {
    case shops = "shop"
  }
}

// MARK: - Shop
struct Shop: Codable {
  let name: String?
  let address: String?
  let genre: Genre?
  let budget: Budget?
  let capacity: Int?
  let openTime: String?
  let urls: URLs?
  let photo: Photo?

  enum CodingKeys: String, CodingKey {
    case name
    case address
    case genre
    case budget
    case capacity
    case openTime = "open"
    case urls
    case photo
  }
}

// MARK: - Genre
struct Genre: Codable {
  let name: String?
}

// MARK: - Budget
struct Budget: Codable {
  let average: String?
}

// MARK: - URLs
struct URLs: Codable {
  let pc: String?
}

// MARK: - Photo
struct Photo: Codable {
  let mobile: PhotoSizes
}

// MARK: - PhotoSizes
struct PhotoSizes: Codable {
  let l: String?
}

```

### Codableについて

**CodableはSwiftの構造体・クラスなどを簡単にエンコード（データ→JSON）やデコード（JSON→データ）するためのプロトコルです。**

通常APIサーバーはデータをJSON形式で送ってきます。このJSON自体は機械が認識しても人間には理解出来づらい言語になっています。これをCodableは人間にもわかり扱いやすくする役割として働きます。

**Codableを身近なものに例えると通訳者のような存在です。**
例えば外国語で書かれたメニュー表を通訳して「これは、店の名前」「これは予算」「これは営業情報」といった感じで受信し、取得したいデータを定義した構造体に変換(デコード)してくれます。
逆にサーバーからデータを送信したい時(エンコード)もサーバーが理解できるJSONに変換し、アプリがデータを正確に送付してくれる仕組みになってます。

https://capibara1969.com/2551/#toc2

https://qiita.com/s_emoto/items/deda5abcb0adc2217e86

### CodingKeyとCodingKeysについて
CodingKeyとCodingKeysは似て非なるものです。ここでは違いについて記載します。

#### CodingKey
CodingKeyはプロトコルで、エンコードやデコードの際に使用されるキーを定義するためのものです。このプロトコルを採用することで、カスタムのキーを指定することができます。
CodingKeyプロトコルには、キーを文字列または整数として表現するためのプロパティが含まれています。
例えば、JSONのキーをSwiftのプロパティ名と異なるものにしたい場合で使用します。


#### CodingKeys
CodingKeysは、Codableプロトコルに準拠した型の中で、エンコードやデコードの際に使用するキーを定義するためのネストされた列挙型です。
CodingKeysはCodingKeyプロトコルに準拠しており、通常は自動生成されますが、カスタムのキーを指定したい場合には手動で定義することができます。
1. Codableに準拠した型では、SwiftがCodbleKeysの列挙型を自動的に探し、キーのカスタマイズがあるかどうかを確認します
2. CodingKeysが見つかれば、その内容を使用してカスタムキー（プロパティ名と異なるキーなど）をエンコード・デコードの際に適用します
***※CodingKeysが定義されていない場合は、Codableプロトコルで定義したプロパティ名をそのままキーとして使用します。***

例えば、JSONのキーとSwiftのプロパティ名が異なる場合に、CodingKeysを使ってマッピングを行います。


```Swift:
// 例
struct Shop: Codable {
    let name: String
    let address: String

    enum CodingKeys: String, CodingKey {
        case name = "shop_name"
        case address = "shop_address"
    }
}
```

## APIClientファイル
指定したURL（ホットペッパーURL）に対しリクエストを行います。受付け係になり「情報をください」と依頼する役割を担います。
もしURLが無効だったり、エラーが発生した場合はエラー情報を返します。リクエストが成功してデータを受け取った場合は、そのデータを返します。

```Swift:APIClient
import Foundation

final class APIClient {
    
    /// シングルトンインスタンス
    static let shared = APIClient()
    
    private init() {}
    
    /// GETリクエスト
    func getRequest(urlString: String, completion: @escaping (Result<Data, Error>) -> Void) {
        guard let url = URL(string: urlString) else {
            completion(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: nil)))
            return
        }
        
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            
            guard let data = data else {
                completion(.failure(NSError(domain: "No data", code: 0, userInfo: nil)))
                return
            }
            
            completion(.success(data))
        }
        
        task.resume()
    }
}
```

`APIClient`が実際にアプリ外部のサーバーなどにAPIデータを依頼する受付け係の存在です。
`getRequest`メソッドがリクエストを行います
`completionハンドラ`で受付からの情報を確認し、その結果次第で情報を渡す、エラーを提示するなど回答を行います。

また`シングルトンパターン`と言われるデザインパターンを採用してます。このクラスでしかデータ通信を行ううことができないようにしています。

## GourmetSearchRequest
指定したキーワードによってグルメ情報をインターネット上で探し取得する処理を担います。ここでは「誰かにグルメ情報を教えて」と依頼し、その情報を待ち貰っているようなイメージです。

```Swift:GourmetSearchRequest
import Foundation
import RxSwift

/// グルメサーチAPIをリクエストするクラス
final class GourmetSearchRequest {
    
    private let key = "受け取ったAPIキー"
    
    /// グルメを検索する
    func searchGourmet(keyword: String) -> Single<Result<Results, Error>> {
        return Single.create { single in
            let path = "https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key=\(self.key)&keyword=\(keyword)&format=json"
            APIClient.shared.getRequest(urlString: path) { result in
                switch result {
                case .success(let jsonData):
                    // デコード処理
                    do {
                        // デコード成功
                        let decoder = JSONDecoder()
                        let response = try decoder.decode(GourmetResponse.self, from: jsonData)
                        // 整形されたJSONのプリント
                        if let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []),
                           let prettyJsonData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
                           let prettyPrintedString = String(data: prettyJsonData, encoding: .utf8) {
                            print("GET request successful: \(prettyPrintedString)")
                        } else {
                            print("GET request successful: \(response)")
                        }
                        
                        // 呼び出し元へ取得データを渡す
                        single(.success(.success(response.results)))
                    } catch {
                        // デコード失敗
                        print("JSON Decoding failed: \(error.localizedDescription)")
                        // 呼び出し元へエラーを渡す
                        single(.failure(error))
                    }
                case .failure(let error):
                    print("GET request failed: \(error.localizedDescription)")
                    // 呼び出し元へエラーを渡す
                    single(.failure(error))
                }
            }
            // リクエストがキャンセルされた時の処理をここに書く
            return Disposables.create()
        }
    }
}
```

`searchGourmet(keyword:)`はグルメ情報を検索するメソッドです。ホットペッパーAPIデータを取得するためのURLとキーワードを組み合わせてURLを生成し、成功か失敗の判定で処理を返します。

受付係のAPIClientファイルから値を取得し、APIデータが届いたらそれを実際に解析(デコード)を行い、解析時に通信エラーなど発生したら、do-catch文でエラー処理を走らせるようにしてます。


- `Single`はRxSwiftが提供しているもので、`Single.create`を使うことで戻り値である`Single<Result<Results, Error>>`型を一度だけ非同期で値を取得するか、エラーを返却します
- `Results`は「成功」と「失敗」を表したSwift提供の列挙型です。Resultsは二つのcase値を持ちます
    - switch文と一緒に使用することで簡単に成功した時と失敗した時の処理を書くことができます。今回であればグルメ情報データの取得が成功か失敗の判定させるように実装しました
        - .success(_:)
            - 成功した時の処理を実装
        - .failure(_:)
            - 失敗した時の処理を実装

https://zenn.dev/dena/articles/eb89f97beecd38

https://qiita.com/st43/items/d4b9a62d93af7c3ade9f


# Part1のまとめ
今回はAPIを取得しデータを受取るまでの処理を纏めてみました。取得したいAPIデータを定義するGourmetResponseファイルの項目は、実際にはもっと多くありますが今回は利用するものだけを定義しました。

APIデータの取得方法は他にも色々なパターンがあるみたいですので試してみたいと思います。