URLSessionを使ってAPIクライアントを実装する

## 概要
APIクライアント実装のメモです。

## 環境
`Xcode 13.1`
以下の内容はXcodeのPlayground環境を使っています。

### リクエストするための`protocol`を実装する
GET・POSTリクエストを`enum`で定義します。
APIのURL, パス, HTTPメソッド, レスポンスを表現するプロトコルを実装します。

```swift:ApiSessionPlayground
enum HttpMethod: String {
    case `get` = "GET"
    case post = "POST"
}

protocol Requestable {
    associatedtype Response: Decodable
    var baseURL: URL { get }
    var path: String { get }
    var httpMethod: HttpMethod { get }
}
```

### レスポンスを`Result<T>`で表現する
APIレスポンスの結果は成功か失敗かで表現します。
`enum`を定義します。

```swift:ApiSessionPlayground
enum Result<T> {
    case success(T)
    case failure(Error)
}
```

### APIリクエストを実行するクラスを定義する
`Session`というクラスを定義しました。
API通信が終わった後にコールバックを実行します。
コールバックの引数には先ほど定義した`Result<T>`が入る想定です。

```swift:ApiSessionPlayground
final class Session {
    func send<T: Requestable>(_ request: T, completion: @escaping (Result<T.Response>) -> ()) {
    }
}
```

### `URLRequest`を作成する
APIのURL, パス, HTTPメソッドから`URLRequest`を作成します。

```swift:ApiSessionPlayground
final class Session {
    func send<T: Requestable>(_ requestable: T, completion: @escaping (Result<T.Response>) -> ()) {
        let url = requestable.baseURL.appendingPathComponent(requestable.path)
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = requestable.httpMethod.rawValue
    }
}
```

### `URLSessionDataTask`を準備する
先程の`URLRequest`からタスクを作成し実行します。

```swift:ApiSessionPlayground
final class Session {
    func send<T: Requestable>(_ requestable: T, completion: @escaping (Result<T.Response>) -> ()) {
        let url = requestable.baseURL.appendingPathComponent(requestable.path)
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = requestable.httpMethod.rawValue
        
        let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
        }
        
        task.resume()
    }
}

```

### API通信時に発生するエラーを定義する
エラーをよりきめ細かく検知するためにカスタムエラーを定義します。

```swift:ApisessionPlayground
enum SessionError: Error {
    case noResponse
    case unacceptableStatuCode(Int)
    case noData
}
```

### APIから受け取ったデータをハンドリングする
以下のことを行なっています。
1. レスポンスを `HTTPURLResponse`に変換する
2. ステータスコードを確認する
3. データをデコードする

```swift:ApiSessionPlayground
final class Session {
    func send<T: Requestable>(_ requestable: T, completion: @escaping (Result<T.Response>) -> ()) {
        let url = requestable.baseURL.appendingPathComponent(requestable.path)
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = requestable.httpMethod.rawValue
        
        let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }

            guard let response = response as? HTTPURLResponse else {
                completion(.failure(SessionError.noResponse))
                return
            }
            
            guard 200..<300 ~= response.statusCode else {
                completion(.failure(SessionError.unacceptableStatuCode(response.statusCode)))
                return
            }
            
            guard let data = data else {
                completion(.failure(SessionError.noData))
                return
            }
            
            do {
                let objects = try JSONDecoder().decode(T.Response.self, from: data)
                completion(.success(objects))
            } catch {
                completion(.failure(error))
            }
        }
        
        task.resume()
    }
}
```

### QiitaのAPIを叩いてみる
QiitaAPIを叩くための構造体を定義します。
また、Qiitaの投稿を表現する構造体を定義します。

```swift:ApiSessionPlayground
struct Article: Codable {
    let title: String
}

struct ArticlesApiRequestable: Requestable {
    typealias Response = [Article]
    var baseURL: URL = URL(string: "https://qiita.com")!
    var path: String = "/apiv2/items"
    var httpMethod: HttpMethod = .get
}

```

### APIリクエストを実行する。
`getArticles()`という関数を定義して、APIをコールします。

```swift:ApiSessionPlayground
func getArticles() {
    let requestable = ArticlesApiRequestable()
    let session = Session()
    session.send(requestable) { result in
        switch result {
        case .success(let articles):
            print(articles.count)
        case .failure(let error):
            print(error)
        }
    }
}

getArticles()
```

全体のコードは以下のようになります。

```swift:ApiSessionPlayground
import Foundation

enum SessionError: Error {
    case noResponse
    case unacceptableStatuCode(Int)
    case noData
}

enum Result<T> {
    case success(T)
    case failure(Error)
}

enum HttpMethod: String {
    case `get` = "GET"
    case post = "POST"
}

protocol Requestable {
    associatedtype Response: Decodable
    var baseURL: URL { get }
    var path: String { get }
    var httpMethod: HttpMethod { get }
}

final class Session {
    func send<T: Requestable>(_ requestable: T, completion: @escaping (Result<T.Response>) -> ()) {
        let url = requestable.baseURL.appendingPathComponent(requestable.path)
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = requestable.httpMethod.rawValue
        
        let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }

            guard let response = response as? HTTPURLResponse else {
                completion(.failure(SessionError.noResponse))
                return
            }
            
            guard 200..<300 ~= response.statusCode else {
                completion(.failure(SessionError.unacceptableStatuCode(response.statusCode)))
                return
            }
            
            guard let data = data else {
                completion(.failure(SessionError.noData))
                return
            }
            
            do {
                let objects = try JSONDecoder().decode(T.Response.self, from: data)
                completion(.success(objects))
            } catch {
                completion(.failure(error))
            }
        }
        
        task.resume()
    }
}

struct Article: Codable {
    let title: String
}

struct ArticlesApiRequestable: Requestable {
    typealias Response = [Article]
    var baseURL: URL = URL(string: "https://qiita.com")!
    var path: String = "/api/v2/items"
    var httpMethod: HttpMethod = .get
}

func getArticles() {
    let requestable = ArticlesApiRequestable()
    let session = Session()
    session.send(requestable) { result in
        switch result {
        case .success(let articles):
            print(articles.count)
        case .failure(let error):
            print(error)
        }
    }
}

getArticles()

```

## 参考リンク

https://peaks.cc/books/iOS_architecture