✔️実装パターン①
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("失敗")
}
}
}
}
少しでも参考になりましたら幸いです😌