[swift5] Essayez de créer un client API avec différentes méthodes

introduction

Cet article est un article pour essayer de créer un client API de différentes manières. (Laissez le titre) En guise de motivation, j'ai voulu écrire sur l'existence d'un framework asynchrone et comment la différence entre Rx et Combine apparaît dans la partie communication. Alors, créez un client API minimum avec les 3 modèles suivants.

  1. URLSession + Codable

Environnement d'exécution

Les demandes sont courantes et utilisent la même chose. C'est un point de terminaison pour obtenir des informations utilisateur de l'API github.

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

Corps du client

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

Comment utiliser

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

Impressions

Passez la fermeture et exécutez après la communication. C'est un style d'écriture courant. Dans le cas d'un projet qui n'introduit pas de framework asynchrone tel que Combine, cela ressemble partout.

②URLSession + RxSwift + Codable

Corps du client


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

Comment utiliser

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

Impressions

Lorsque vous recevez le résultat observable après la communication, transmettez la valeur à Behavior Relay. Vous pouvez écrire à peu près la même chose que l'implémentation qui passe la clôture de l'achèvement à la demande. Après cela, si vous observez le relais, il semble que vous puissiez facilement implémenter le rappel vers ViewModel et View. Sans Rx, la partie où le résultat est surveillé par didSet et la fermeture est implémentée par la fermeture est déclarative.

③URLSession + Combine + Codable

Corps du client


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

Comment utiliser

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)

Impressions

Future est un éditeur qui ne publie des valeurs qu'une seule fois, et cela ressemble à celui de RxSwift. Référence: Remplacer RxSwift Single par Combine-Qiita

Combine vous permet de définir le type d'erreur dans les génériques afin que vous n'ayez pas à convertir l'erreur. De plus, Future peut prendre le type Result comme argument avec la fermeture de fin de .init.

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

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

Donc, il est bon de pouvoir gérer simplement l'achèvement avec promise (.success (entity)).

Les valeurs émises par Future sont reçues par think et gérées par recieveValue et receiveCompletion. Personnellement, je me sens mal à l'aise que le traitement des valeurs et le traitement des résultats soient séparés. (Utiliser Never = S'il n'y a pas d'échec, vous ne pouvez utiliser que receiveValue think.)

Comme l'implémentation qui passe la fermeture du type de résultat à la fin, ce serait bien si la réponse pouvait également être reçue avec la valeur associée du cas de réussite, mais lors de l'utilisation de combine, cela semble être strict car il est nécessaire d'utiliser le think ci-dessus.

Sommaire

Quelle que soit la méthode d'écriture adoptée, le client API lui-même semblait similaire. Donc même si vous décidez d'introduire un framework asynchrone depuis le milieu, il semble que vous n'aurez pas peur de la partie communication. (Au contraire, il peut être nécessaire de passer activement d'un client API proche du modèle)

Recommended Posts

[swift5] Essayez de créer un client API avec différentes méthodes
Un exemple de client API Moya + RxSwift (Swift5)
Essayez d'obtenir la clé API de redmine avec ruby
NLP4J [006-034b] Essayez de faire en sorte que l'annotateur de 100 traitements de langage frappe # 34 "A B" avec NLP4J
Essayez de faire un programme d'addition en plusieurs langues
Écrivons comment créer une API avec SpringBoot + Docker à partir de 0
[Débutant] Essayez de créer un jeu RPG simple avec Java ①
Comment créer une application à l'aide de Tensorflow avec Android Studio
J'ai essayé de créer une application Android avec MVC maintenant (Java)
Créer un compteur FPS avec Swift
Essayez de faire un simple rappel
CompletableFuture Getting Started 2 (Essayez de faire CompletableFuture)
Essayez de créer un itérateur qui puisse être vu
J'ai essayé de faire une sauvegarde automatique avec plus agréable + PostgreSQL + SSL + docker
[iOS] J'ai essayé de créer une application de traitement de type insta avec Swift
J'ai essayé de créer une API Web qui se connecte à DB avec Quarkus
Comment convertir un tableau de chaînes en un tableau d'objets avec l'API Stream
Comment créer une application avec un mécanisme de plug-in [C # et Java]
Feign, qui implémente un client API avec juste une interface, est très pratique!
Comment recadrer une image avec libGDX
Je veux créer une application ios.android
Essayez d'implémenter une fonction de connexion avec Spring-Boot
Faisons un écran d'erreur avec Rails
Rails6 Je veux créer un tableau de valeurs avec une case à cocher
Essayez de faire un rapport chronologique du temps d'exécution d'une méthode à l'aide de l'API JFR