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