2
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-18

✔️実装パターン①

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

Protocolでリクエストを作成

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

import Foundation

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

UserResponse

import Foundation

struct UserResponse: Decodable {
    var result: User

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

APIClient

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

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()
    }
}

呼び出し元

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

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の作成、リクエストを作成する。

// 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
        }
    }
}

ネットワーク通信をするクラス

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."
        }
    }
}

呼び出し元

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("失敗")
            }
        }
    }
}

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

参考

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