[swift5] いろんな作り方でAPIクライアントを作ってみる

## はじめに
本記事は色んな作り方でAPIクライアントを作ってみる記事です。(タイトルまま)
モチベーションとしては非同期フレームワークのありなしやRxとCombineの違いが通信部分でどう出てくるのか書いてみて試したかった感じです。
なので以下の３パターンでミニマムなAPIクライアントを作ります。

1. URLSession + Codable
* URLSession + RxSwift + Codable
* URLSession + Combine + Codable

## 実行環境
* Xcode 12
* Swift 5.3

リクエストは共通で同じものを使います。githubAPIのuser情報取得のエンドポイントです。

```swift
struct GetUserRequest: BaseRequest {

    typealias Response = UserResponse
        
    var path: String { "/users" + "/" + username}
    
    var method: HttpMethod { .get }
    
    let username: String
            
    struct Request: Encodable {}
}

struct UserResponse: Decodable {
    var login: String
    var id: Int
}

```

## ①URLSession + Codable
### クライアント本体
```swift
protocol BaseRequest {
    associatedtype Request: Encodable
    associatedtype Response: Decodable
    
    var baseUrl: String { get }
    
    var path: String { get }
    
    var url: URL? { get }
    
    var method: HttpMethod { get }
    
    var headerFields: [String: String] { get }
    
    var encoder: JSONEncoder { get }
    
    var decoder: JSONDecoder { get }
    
    func request(_ parameter: Request?, completionHandler: ((Result<Response, APIError>) -> Void)?)
}

extension BaseRequest {
    
    var baseUrl: String { "https://api.github.com" }

    var url: URL? { URL(string: baseUrl + path) }
    
    var headerFields: [String: String] { [String: String]() }
    
    var defaultHeaderFields: [String: String] { ["content-type": "application/json"] }
    
    var encoder: JSONEncoder {
        let encoder = JSONEncoder()
        encoder.keyEncodingStrategy = .convertToSnakeCase
        return encoder
    }
    
    var decoder: JSONDecoder {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return decoder
    }

    func request(_ parameter: Request? = nil, completionHandler: ((Result<Response, APIError>) -> Void)? = nil) {
        do {
            let data = parameter == nil ? nil : try encoder.encode(parameter)
            request(data, completionHandler: completionHandler)
        } catch {
            completionHandler?(.failure(.request))
        }
    }
    
    func request(_ data: Data?, completionHandler: ((Result<Response, APIError>) -> Void)? = nil) {
        do {
            guard let url = url, var urlRequest = try method.urlRequest(url: url, data: data) else { return }
            urlRequest.allHTTPHeaderFields = defaultHeaderFields.merging(headerFields) { $1 }
            urlRequest.timeoutInterval = 8
            
            var dataTask: URLSessionTask!
            dataTask = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
                if let error = error {
                    completionHandler?(.failure(.responseError(nsError)))
                    return
                }
                
                guard let data = data, let response = response as? HTTPURLResponse else {
                    completionHandler?(.failure(.emptyResponse))
                    return
                }
                
                guard 200..<300 ~= response.statusCode else {
                    completionHandler?(.failure(.http(status: response.statusCode)))
                    return
                }
                
                do {
                    let entity = try self.decoder.decode(Response.self, from: data)
                    completionHandler?(.success(entity))
                } catch {
                    completionHandler?(.failure(.decode))
                }
            }
            dataTask.resume()
        } catch {
            completionHandler?(.failure(.request))
        }
    }
}

enum APIError: Error {
    case request
    case response(error: Error? = nil)
    case emptyResponse
    case decode(Error)
    case http(status: Int, data: Data)
}

enum HttpMethod: String {
    case get = "GET"
    case post = "POST"
    case put = "PUT"
    case delete = "DELETE"
    case patch = "PATCH"
    
    func urlRequest(url: URL, data: Data?) throws -> URLRequest? {
        var request = URLRequest(url: url)
        switch self {
        case .get:
            guard let data = data else {
                request.httpMethod = rawValue
                return request
            }
            
            guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true),
                let dictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { return nil }
            
            components.queryItems = dictionary.map { URLQueryItem(name: $0.key, value: "\($0.value)") }
            guard let getUrl = components.url else { return nil }
            var request = URLRequest(url: getUrl)
            request.httpMethod = rawValue
            return request
            
        case .post, .put, .delete, .patch:
            request.httpMethod = rawValue
            request.httpBody = data
            return request
        }
    }
}
```

### 利用方法
```swift
GetUserRequest(username: "hoge").request(.init()) { [weak self] result in
    switch result {
    case .success(let response):
        guard let self = self else { return }
        self.response = response
                
    case .failure(let error):
        self.showAlert(message: error.localizedDescription)
    }
}
```

### 感想
クロージャを渡しておいて通信後に実行します。よくみる書き方です。
Combineなどの非同期フレームワークを導入しないプロジェクトの場合、どこもこんな感じになりそうです。


## ②URLSession + RxSwift + Codable
### クライアント本体
```swift

extension BaseRequest {
    
...

    private func request(_ data: Data?) -> Single<Response> {
        return Single.create(subscribe: { observer -> Disposable in
            do {
                guard let url = self.url, var urlRequest = try self.method.urlRequest(url: url, data: data) else {
                    return Disposables.create()
                }
                urlRequest.allHTTPHeaderFields = self.defaultHeaderFields.merging(self.headerFields) { $1 }
                urlRequest.timeoutInterval = 8
                
                let dataTask = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
                    if let error = error {
                        observer(.error(error))
                    }
                    
                    guard let data = data, let response = response as? HTTPURLResponse else {
                        observer(.error(APIError.response))
                        return
                    }
                    
                    guard 200..<300 ~= response.statusCode else {
                        observer(.error(APIError.http(status: response.statusCode, data: data)))
                        return
                    }
                    
                    do {
                        let entity = try self.decoder.decode(Response.self, from: data)
                        observer(.success(entity))
                    } catch {
                        observer(.error(APIError.decode(error)))
                    }
                }
                dataTask.resume()
                return Disposables.create()

            } catch {
                return Disposables.create()
            }
        })
    }
}
```

### 利用方法
```swift
private let resultRelay: BehaviorRelay<Result<LoginResponse, Error>?> = BehaviorRelay(value: nil)
private let disposeBag: DisposeBag = DisposeBag()

GetUserRequest(username: "hoge").request(.init())
    .subscribe(onSuccess: { response in
        self.resultRelay.accept(.success(response))

    }, onError: { error in
        self.resultSubject.accept(.failure(error))

    })
    .disposed(by: disposeBag)

var result: Observable<Result<LoginResponse, Error>?> {
    return resultRelay.asObservable()
}
```

### 感想
通信後の結果のobservableを受け取ったらBehaviorRelayに値を渡します。
ほぼcompletionのクロージャをリクエストに渡す実装と変わらずに書けますね。
あとはrelayをobserveしておけばViewModelやViewへのコールバックが楽に実装できそうです。
RxなしだとdidSetで結果を監視したりクロージャにクロージャが連続したりで実装する箇所が、宣言的になっています。

## ③URLSession + Combine + Codable

### クライアント本体
```swift

extension BaseRequest {
    
...
    
    private func request(_ data: Data?) -> Future<Response, APIError> {
        return .init { promise in
            do {
                guard let url = self.url,
                      var urlRequest = try self.method.urlRequest(url: url, data: data) else {
                    return
                }
                urlRequest.allHTTPHeaderFields = self.defaultHeaderFields.merging(self.headerFields) { $1 }
                urlRequest.timeoutInterval = 8
                
                let dataTask = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
                    if let error = error {
                        promise(.failure(.response(error: error)))
                    }
                    
                    guard let data = data, let response = response as? HTTPURLResponse else {
                        promise(.failure(.response()))
                        return
                    }
                    
                    guard 200..<300 ~= response.statusCode else {
                        promise(.failure(.http(status: response.statusCode, data: data)))
                        return
                    }
                    
                    do {
                        let entity = try self.decoder.decode(Response.self, from: data)
                        promise(.success(entity))
                    } catch {
                        promise(.failure(APIError.decode(error)))
                    }
                }
                dataTask.resume()
            } catch {
                promise(.failure(error))
            }
        }
    }
}

```

### 利用方法

```swift
private var binding = Set<AnyCancellable>()
@Published private(set) var response: Response?

let exp = expectation(description: "Success")

GetUserRequest(username: "hoge").request(.init())
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            // do something when loading finished.
        case .failure(let error):
            // do something when the error occured.
        }
    }, receiveValue: { [weak self] response in

        guard let self = self else { return }

        self.response = response

    }).store(in: &binding)
        
waitForExpectations(timeout: 20)
```

### 感想

Futureは一度だけ値を発行するPublisherで、RxSwiftのSingleみたいな感じです。
参考：[Combine で RxSwift の Single を置きかえる - Qiita](https://qiita.com/kumamotone/items/15e5a580a9eba6be189d)

Combineはジェネリクスでエラーのタイプを定義できるのでエラーをキャストする必要がありません。
また、Futureは.initのtrailing closureでResult型を引数にとれます。

```swift
public typealias Promise = (Result<Output, Failure>) -> Void

public init(_ attemptToFulfill: @escaping (@escaping Future<Output, Failure>.Promise) -> Void)
```

なので、`promise(.success(entity))`でシンプルに完了時のハンドリングできるのが良いですね。

Futureから発行された値はsinkで受け取り、`recieveValue`と`receiveCompletion`でハンドリングします。
個人的には値のハンドリングと結果のハンドリング分かれているのが気持ち悪いです。
(Neverを使っている＝失敗はない場合は`receiveValue`のみのsinkが使用できます。)

Result型のクロージャをcompletionに渡しあげる実装のように、successのcaseのassociatedValueでresponseも受け取れたらいいんですが、combineを使う場合は上記のsinkを利用する必要があるので厳しそうです。

## まとめ
どの書き方を採用してもAPIクライアント自体は似たような作りになりそうでした。
なので途中から非同期フレームワークを導入することになっても通信部分で恐ることはなさそうです。(むしろモデルに近いAPIクライアントから積極的に変えていくべきかも)