[swift5] Versuchen Sie, einen API-Client auf verschiedene Arten zu erstellen

Einführung

Dieser Artikel ist ein Artikel, in dem versucht wird, einen API-Client auf verschiedene Arten zu erstellen. (Hinterlasse den Titel) Als Motivation wollte ich versuchen, über die Existenz eines asynchronen Frameworks zu schreiben und darüber, wie der Unterschied zwischen Rx und Combine im Kommunikationsteil auftritt. Erstellen Sie also einen minimalen API-Client mit den folgenden 3 Mustern.

  1. URLSession + Codable

Ausführungsumgebung

Anfragen sind häufig und verwenden dasselbe. Es ist ein Endpunkt zum Abrufen von Benutzerinformationen der Github-API.

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

Kundenkörper

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

Wie benutzt man

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

Impressionen

Übergeben Sie die Schließung und führen Sie sie nach der Kommunikation aus. Dies ist ein gängiger Schreibstil. Bei einem Projekt, das kein asynchrones Framework wie Combine einführt, sieht es überall so aus.

②URLSession + RxSwift + Codable

Kundenkörper


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

Wie benutzt man

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

Impressionen

Wenn Sie nach der Kommunikation das beobachtbare Ergebnis erhalten, übergeben Sie den Wert an Behavior Relay. Sie können fast dasselbe schreiben wie die Implementierung, die den Abschluss der Fertigstellung an die Anforderung weitergibt. Wenn Sie danach das Relay beobachten, können Sie den Rückruf an ViewModel und View anscheinend problemlos implementieren. Ohne Rx ist der Teil, in dem das Ergebnis von didSet überwacht und der Abschluss durch den Abschluss implementiert wird, deklarativ.

③URLSession + Combine + Codable

Kundenkörper


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

Wie benutzt man

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)

Impressionen

Future ist ein Verlag, der Werte nur einmal veröffentlicht und sich wie die Single von RxSwift anfühlt. Referenz: RxSwift Single durch Combine-Qiita ersetzen

Mit Kombinieren können Sie die Art des Fehlers in den Generika definieren, damit Sie den Fehler nicht umwandeln müssen. Darüber hinaus kann Future den Ergebnistyp als Argument mit dem nachfolgenden Abschluss von .init verwenden.

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

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

Es ist also gut, die Fertigstellung einfach mit "Versprechen (.success (Entität))" abwickeln zu können.

Der von Future ausgegebene Wert wird von sink empfangen und von "recieveValue" und "receiveCompletion" verarbeitet. Persönlich fühle ich mich unwohl, dass der Umgang mit Werten und der Umgang mit Ergebnissen getrennt sind. (Verwenden von Never = Wenn kein Fehler vorliegt, können Sie nur "receiveValue" think verwenden.)

Wie bei der Implementierung, bei der der Abschluss des Ergebnistyps vollständig abgeschlossen wird, wäre es schön, wenn die Antwort auch mit dem zugehörigen Wert des Erfolgsfalls empfangen werden könnte. Bei Verwendung von Mähdrescher scheint dies jedoch streng zu sein, da der obige Gedanke verwendet werden muss.

Zusammenfassung

Unabhängig davon, welche Schreibmethode angewendet wurde, schien der API-Client selbst ähnlich zu sein. Selbst wenn Sie sich entscheiden, ein asynchrones Framework von der Mitte aus einzuführen, haben Sie anscheinend keine Angst vor dem Kommunikationsteil. (Möglicherweise muss ein API-Client, der sich in der Nähe des Modells befindet, aktiv geändert werden.)

Recommended Posts

[swift5] Versuchen Sie, einen API-Client auf verschiedene Arten zu erstellen
Ein Beispiel für einen Moya + RxSwift-API-Client (Swift5)
Versuchen Sie, den API-Schlüssel von Redmine mit Ruby zu erhalten
NLP4J Versuchen Sie, Annotator von 100 Sprachverarbeitung mit NLP4J auf # 34 "A B" zu bringen
Versuchen Sie, ein Zusatzprogramm in mehreren Sprachen zu erstellen
Lassen Sie uns schreiben, wie API mit SpringBoot + Docker von 0 erstellt wird
[Anfänger] Versuchen Sie, mit Java ein einfaches RPG-Spiel zu erstellen ①
So erstellen Sie eine App mit Tensorflow mit Android Studio
Ich habe versucht, eine Android-Anwendung mit MVC zu erstellen (Java)
Machen Sie mit Swift einen FPS-Zähler
Versuchen Sie, einen einfachen Rückruf zu tätigen
CompletableFuture Erste Schritte 2 (Versuchen Sie, CompletableFuture zu erstellen)
Versuchen Sie, einen Iterator zu erstellen, der einen Blick darauf werfen kann
Ich habe versucht, ein automatisches Backup mit angenehmem + PostgreSQL + SSL + Docker zu erstellen
[iOS] Ich habe versucht, mit Swift eine insta-ähnliche Verarbeitungsanwendung zu erstellen
Ich habe versucht, eine Web-API zu erstellen, die mit Quarkus eine Verbindung zur Datenbank herstellt
So konvertieren Sie ein Array von Strings mit der Stream-API in ein Array von Objekten
So erstellen Sie eine App mit einem Plug-In-Mechanismus [C # und Java]
Feign, das einen API-Client nur mit einer Schnittstelle implementiert, ist sehr praktisch!
So beschneiden Sie ein Bild in libGDX
Ich möchte eine ios.android App machen
Versuchen Sie, eine Anmeldefunktion mit Spring-Boot zu implementieren
Lassen Sie uns mit Rails einen Fehlerbildschirm erstellen
Rails6 Ich möchte ein Array von Werten mit einem Kontrollkästchen erstellen
Versuchen Sie, mithilfe der JFR-API einen Zeitleistenbericht über die Ausführungszeit einer Methode zu erstellen