APIKitなどのライブラリを使わずに標準のURLSessionで通信処理を実装をする

# ✔️実装パターン①

**RequestとResponseの関係を一対一にする。**

## Protocolでリクエストを作成

```swift
import Foundation

protocol APIRequest {
    associatedtype Responses: Decodable
    associatedtype Parameters: Encodable

    var path: String { get }
    var method: String { get }
    var headers: String? { get }
    var queries: [URLQueryItem]? { get set }
    var body: Parameters? { get set }
}

struct Request {
    struct Login: APIRequest {
        typealias Responses = UserResponse
        typealias Parameters = UserRequest

        let path = "/login"
        let method = "POST"
        let headers: String? = nil
        var queries: [URLQueryItem]?
        var body: UserRequest?
    }

    struct Signup: APIRequest {
        typealias Responses = UserResponse
        typealias Parameters = UserRequest

        let path = "/sign_up"
        let method = "POST"
        let headers: String? = nil
        var queries: [URLQueryItem]?
        var body: UserRequest?
    }
}
```

### UserRequest

```swift
import Foundation

struct UserRequest: Encodable {
    var email: String
    var password: String
}
```

### UserResponse

```swift
import Foundation

struct UserResponse: Decodable {
    var result: User
    
    struct User: Decodable {
        var id: Int
        var email: String
        var token: String
    }
}
```

## APIClient
APIRequestに準拠している構造体を引数に入れる。
typeによって処理を切り替える。

```swift
import Foundation

struct APIClient {
    static func sendRequest<T: APIRequest>(
        from type: T,
        completion: @escaping (Result<T.Responses, Error>) -> Void) {

        let BaseURL: String = "http://xxxxxxxxx"

        func createRequest() -> URLRequest? {
            guard var components = URLComponents(string: "\(BaseURL)\(type.path)") else { return  nil}

            if type.method == "GET" {
                let queryItems = type.queries
                components.queryItems = queryItems
            }

            guard let url = components.url else { return nil}
            var request = URLRequest(url: url)
            request.httpMethod = type.method

            if type.method != "GET" {
                let httpBody = JSONEncoder().encode(value: type.body)
                request.httpBody = httpBody
            }

            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.setValue(type.headers, forHTTPHeaderField: "access_token")
            return request
        }

        guard let request = createRequest() else { return }

        let task = URLSession.shared.dataTask(with: request) { (data, res, error) in
            if let error = error {
                completion(.failure(error))
            }

            guard let data = data else {
                print("no data")
                return
            }

            guard let res = res as? HTTPURLResponse, (200...299).contains(res.statusCode) else { return }

            do {
                let jsonDecoder = JSONDecoder()
                jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
                let json = try jsonDecoder.decode(T.Responses.self, from: data)
                DispatchQueue.main.sync {
                    completion(.success(json))
                }
            } catch {
                DispatchQueue.main.sync {
                    completion(.failure(error))
                }
            }
        }
        task.resume()
    }
}
```

## 呼び出し元

```swift
import UIKit

class ViewController: UIViewController {

    typealias UserRequest = Request.Login

    override func viewDidLoad() {
        super.viewDidLoad()

        let params = UserRequest.Parameters(
            email: "xxx.com",
            password: "xxxxx"
        )

        let queryItems: [URLQueryItem] = {
            var queryItems = [URLQueryItem]()
            queryItems.append(URLQueryItem(name: "email", value: params.email))
            queryItems.append(URLQueryItem(name: "password", value: params.password))
            return queryItems
        }()

        APIClient.sendRequest(from: UserRequest(queries: queryItems)) { (result) in
            switch result {
            case .success(let response):
                print("success", response)
            case .failure:
                print("failure")
            }
        }
    }
}
```

## extension
```swift
import Foundation

extension JSONEncoder {
    func encode<T: Encodable>(value: T) -> Data? {
        self.keyEncodingStrategy = .convertToSnakeCase
        let encodeValue = try? self.encode(value)
        return encodeValue
    }
}

```

# ✔️実装パターン② アンチパターン
最初は以下のように書いていたのですが、実はアンチパターンのようでした。
理由としては以下の通りです。

- Requestに対して引数にResponseの型を入れる。
- リクエストをする際にリクエスト・レスポンスを入力する必要があると、間違えて入力した際にレスポンスが受け取れなくなる、なのでリクエストとレスポンスは一対一にするのがよさそう。

## URLRouter
URLSessionの作成、リクエストを作成する。

```swift
// URLの向き先を作成する型、エンドポイントを追加したい時やHTTPの設定はこの型を参照する。
enum URLRouter {
    case addBook
    case editBook(id: Int, body: [String: Any])

    private static let baseURLString = "YOUR_BASE_URL_STRING"

    private enum HTTPMethod {
        case get
        case post
        case put
        case delete

        var value: String {
            switch self {
            case .get: return "GET"
            case .post: return "POST"
            case .put: return "PUT"
            case .delete: return "DELETE"
            }
        }
    }

    private var method: HTTPMethod {
        switch self {
        case .addBook: return .get
        case .editBook: return .put
        }
    }

    private var path: String {
        switch self {
        case .addBook:
            return "/books"
        case .editBook(let id):
             return "/books/\(id)"
        }
    }

    func request() throws -> URLRequest {
        let urlString = "\(URLRouter.baseURLString)\(path)"

        guard let url = URL(string: urlString) else {
            throw ErrorType.parseUrlFail
        }

        var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 10)
        request.httpMethod = method.value
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")

        switch self {
        case .addBook:
            return request
        case .editBook(_, let body):
            request.httpBody = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
            return request
        }
    }
}
```

## ネットワーク通信をするクラス
```swift
class Network {
    static let shared = Network()

    private let config: URLSessionConfiguration
    private let session: URLSession

    private init() {
        config = URLSessionConfiguration.default
        session = URLSession(configuration: config)
    }
    
    // リクエスト時にURLRouterを注入する。
    func request<T: Decodable>(router: URLRouter, completion: @escaping (Result<T, Error>) -> ()) {
        do {
            let task = try session.dataTask(with: router.request()) { (data, urlResponse, error) in
                DispatchQueue.main.async {
                    if let error = error {
                        completion(.failure(error))
                        return
                    }

                    guard let statusCode = urlResponse?.getStatusCode(), (200...299).contains(statusCode) else {
                        let errorType: ErrorType

                        switch urlResponse?.getStatusCode() {
                        case 404:
                            errorType = .notFound
                        case 422:
                            errorType = .validationError
                        case 500:
                            errorType = .serverError
                        default:
                            errorType = .defaultError
                        }

                        completion(.failure(errorType))
                        return
                    }

                    guard let data = data else {
                        completion(.failure(ErrorType.defaultError))
                        return
                    }

                    do {
                        let result = try JSONDecoder().decode(T.self, from: data)
                        completion(.success(result))
                    } catch let error {
                        completion(.failure(error))
                    }
                }
            }
            task.resume()

        } catch let error {
            completion(.failure(error))
        }
    }
}

extension URLResponse {
    func getStatusCode() -> Int? {
        if let httpResponse = self as? HTTPURLResponse {
            return httpResponse.statusCode
        }
        return nil
    }
}

enum ErrorType: LocalizedError {
    case parseUrlFail
    case notFound
    case validationError
    case serverError
    case defaultError

    var errorDescription: String? {
        switch self {
        case .parseUrlFail:
            return "Cannot initial URL object."
        case .notFound:
            return "Not Found"
        case .validationError:
            return "Validation Errors"
        case .serverError:
            return "Internal Server Error"
        case .defaultError:
            return "Something went wrong."
        }
    }
}

```

## 呼び出し元
```swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let id = 2525
        // クエリ
        let params: [String: Any] = [
            "first_name": "Amitabh2",
            "last_name": "Bachchan2",
            "email": "ab@bachchan.com",
            "phone_number": "+919980123412",
            "favorite": false
        ]

        Network.shared.request(router: .editBook(id: id, body: params)) { (result: Result<BookEditResponse, Error>) in
            switch result {
            case .success(let item):
                print("成功")
            case .failure(let err):
                print("失敗")
            }
        }
    }
}
```

少しでも参考になりましたら幸いです😌


## 参考
- [Create a Router Network from URLSession | by Kittisak Phetrungnapha | iTopStory](https://itopstory.com/create-a-router-network-from-urlsession-4278c9de6d08)
- [The best way to use REST APIs in Swift | by arteko | Medium](https://medium.com/@arteko/the-best-way-to-use-rest-apis-in-swift-95e10696c980)