Codierbar für API-Anfragen und wie man Code ordnet

Hallo Habr!

Ab Swift 4 haben wir Zugriff auf das neue Codable-Protokoll, mit dem Modelle einfach codiert / decodiert werden können. Meine Projekte enthalten viel Code für API-Aufrufe, und im letzten Jahr habe ich viel Arbeit geleistet, um dieses riesige Code-Array in etwas sehr Leichtes, Prägnantes und Einfaches zu optimieren, indem sich wiederholender Code beendet und Codable auch für mehrteilige Anforderungen und URL-Abfrageparameter verwendet wurde. Meiner Meinung nach stellte sich heraus, dass es mehrere hervorragende Klassen für das Senden von Anfragen und das Parsen von Antworten vom Server gab. Sowie eine praktische Dateistruktur, die die Controller für jede Gruppe von Anforderungen darstellt, die ich bei der Verwendung von Vapor 3 im Backend als Root verwendet habe. Vor einigen Tagen habe ich alle meine Entwicklungen einer separaten Bibliothek zugeordnet und sie CodyFire genannt. Ich möchte in diesem Artikel über sie sprechen.

Haftungsausschluss


CodyFire basiert auf Alamofire, ist jedoch mehr als nur ein Wrapper für Alamofire. Es ist ein ganzheitlicher Systemansatz für die Arbeit mit der REST-API für iOS. Deshalb mache ich mir keine Sorgen, dass Alamofire die fünfte Version sägt, in der es Codable-Unterstützung geben wird, weil es wird meine Schöpfung nicht töten.

Initialisierung


Beginnen wir ein wenig von weitem, nämlich dass wir oft drei Server haben:

dev - für die Entwicklung, was wir von Xcode ausgehen
Stufe - zum Testen vor der Veröffentlichung, normalerweise in TestFlight oder InHouse
Produktproduktion für den AppStore

Und viele iOS-Entwickler sind sich natürlich der Existenz von Umgebungsvariablen und der Startschemata in Xcode bewusst, aber in meiner (über 8-jährigen) Praxis schreiben 90% der Entwickler den richtigen Server während des Testens oder vor dem Zusammenbau manuell in einer Konstanten, und das ist es Was ich beheben möchte, indem ich ein gutes Beispiel dafür zeige, wie man es richtig macht.

Standardmäßig ermittelt CodyFire automatisch, in welcher Umgebung die Anwendung gerade ausgeführt wird. Dies macht es sehr einfach:

#if DEBUG //DEV environment #else if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { //TESTFLIGHT environment } else { //APPSTORE environment } #endif 

Dies ist natürlich unter der Haube, und im Projekt in AppDelegate müssen Sie nur drei URLs registrieren

 import CodyFire @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { let dev = CodyFireEnvironment(baseURL: "http://localhost:8080") let testFlight = CodyFireEnvironment(baseURL: "https://stage.myapi.com") let appStore = CodyFireEnvironment(baseURL: "https://api.myapi.com") CodyFire.shared.configureEnvironments(dev: dev, testFlight: testFlight, appStore: appStore) return true } } 

Und man könnte sich einfach darüber freuen und nichts anderes tun.

Aber im wirklichen Leben müssen wir oft Entwickler-, Bühnen- und Produktserver in Xcode testen, und dafür fordere ich Sie dringend auf, Startschemata zu verwenden.

Bild
Tipp: Vergessen Sie im Abschnitt Schemata verwalten nicht, das Kontrollkästchen "Freigegeben" für jedes Schema zu aktivieren, damit sie allen Entwicklern im Projekt zur Verfügung stehen.

In jedem Schema müssen Sie die Umgebungsvariable "env" schreiben, die drei Werte annehmen kann: dev, testFlight, appStore.

Bild

Damit diese Schemata mit CodyFire funktionieren, müssen Sie AppDelegate.didFinishLaunchingWithOptions nach der Initialisierung von CodyFire den folgenden Code hinzufügen

 CodyFire.shared.setupEnvByProjectScheme() 

Darüber hinaus werden Sie häufig vom Chef oder den Testern Ihres Projekts aufgefordert, den Server im laufenden Betrieb irgendwo auf LoginScreen zu wechseln . Mit CodyFire können Sie dies einfach implementieren, indem Sie den Server in einer Zeile wechseln und die Umgebung ändern:

 CodyFire.shared.environmentMode = .appStore 

Dies funktioniert so lange, bis die Anwendung neu gestartet wird. Wenn Sie möchten, dass sie nach dem Start gespeichert wird, speichern Sie den Wert in UserDefaults , überprüfen Sie, wann die Anwendung in AppDelegate gestartet wird, und wechseln Sie die Umgebung in die erforderliche Umgebung.
Ich sagte zu diesem wichtigen Punkt, ich hoffe, dass es mehr Projekte geben wird, in denen das Umschalten der Umgebung wunderbar durchgeführt wird. Gleichzeitig haben wir die Bibliothek bereits initialisiert.

Dateistruktur und Controller


Jetzt können Sie über meine Vision der Dateistruktur für alle API-Aufrufe sprechen. Dies kann als Ideologie von CodyFire bezeichnet werden.

Mal sehen, wie es im Projekt endlich aussieht

Bild

Schauen wir uns nun die Dateilisten an und beginnen wir mit API.swift .

 class API { typealias auth = AuthController typealias post = PostController } 

Hier werden Links zu allen Controllern aufgelistet, damit sie einfach über "API.controller.method" aufgerufen werden können.

 class AuthController {} 

API + Login.swift

 extension AuthController { struct LoginResponse: Codable { var token: String } static func login(email: String, password: String) -> APIRequest<LoginResponse> { return APIRequest("login").method(.post) .basicAuth(email: email, password: password) .addCustomError(.notFound, "User not found") } } 

In diesem Dekorator deklarieren wir eine Funktion zum Aufrufen unserer API:

- Endpunkt angeben
- HTTP-POST-Methode
- Verwenden Sie den Wrapper für die Basisauthentifizierung
- Deklarieren Sie den gewünschten Text für eine bestimmte Antwort vom Server (dies ist praktisch).
- und geben Sie das Modell an, mit dem die Daten dekodiert werden

Was bleibt verborgen?

- Sie müssen nicht die vollständige Server-URL angeben, da es ist bereits global eingestellt
- musste nicht angeben, dass wir 200 OK erwarten, wenn alles in Ordnung ist
200 OK ist der Standardstatuscode, den CodyFire für alle Anforderungen erwartet. In diesem Fall werden Daten dekodiert und ein Rückruf aufgerufen. Alles ist in Ordnung. Hier sind Ihre Daten.
Außerdem können Sie irgendwo im Code für Ihren LoginScreen einfach anrufen

 API.auth.login(email: "test@mail.com", password: "qwerty").onError { error in switch error.code { case .notFound: print(error.description) //: User not found default: print(error.description) } }.onSuccess { token in //TODO:  auth token    print("Received auth token: "+ token) } 

onError und onSuccess sind nur ein kleiner Teil der Rückrufe, die APIRequest zurückgeben kann. Wir werden später darauf eingehen.

Im Eingabebeispiel haben wir nur die Option berücksichtigt, wenn die zurückgegebenen Daten automatisch dekodiert werden. Sie können jedoch sagen, dass Sie sie selbst implementieren könnten, und Sie haben Recht. Betrachten wir daher die Möglichkeit, Daten gemäß dem Modell am Beispiel des Registrierungsformulars zu senden.

API + Signup.swift

 extension AuthController { struct SignupRequest: JSONPayload { let email, password: String let firstName, lastName, mobileNumber: String init(email: String, password: String, firstName: String, lastName: String, mobileNumber: String) { self.email = email self.password = password self.firstName = firstName self.lastName = lastName self.mobileNumber = mobileNumber } } struct SignupResponse: Codable { let token: String } static func signup(_ request: SignupRequest) -> APIRequest<SignupResponse> { return APIRequest("signup", payload: request).method(.post) .addError(.conflict, "Account already exists") } } 

Im Gegensatz zur Anmeldung übertragen wir bei der Registrierung eine große Datenmenge.

In diesem Beispiel haben wir ein SignupRequest- Modell, das dem JSONPayload- Protokoll entspricht (daher versteht CodyFire den Nutzlasttyp), sodass der Hauptteil unserer Anforderung in Form von JSON vorliegt. Wenn Sie x-www-form-urlencoded benötigen, verwenden Sie FormURLEncodedPayload .

Als Ergebnis erhalten Sie eine einfache Funktion, die das Nutzlastmodell akzeptiert
 API.auth.signup(request) 

und was Ihnen bei Erfolg ein bestimmtes Antwortmodell zurückgibt.

Ich finde es cool, oder?

Aber was ist, wenn mehrteilig?


Schauen wir uns ein Beispiel an, in dem Sie einen Beitrag erstellen können.

Post + Create.swift

 extension PostController { struct CreateRequest: MultipartPayload { var text: String var tags: [String] var images: [Attachment] var video: Data init (text: String, tags: [String], images: [Attachment], video: Data) { self.text = text self.tags = tags self.images = images self.video = video } } struct Post: Codable { let text: String let tags: [String] let linksToImages: [String] let linkToVideo: String } static func create(_ request: CreateRequest) -> APIRequest<CreateRequest> { return APIRequest("post", payload: request).method(.post) } } 

Dieser Code kann ein mehrteiliges Formular mit einer Reihe von Bilddateien und einem Video senden.
Mal sehen, wie man einen Versand anruft. Hier ist der interessanteste Moment über Anhang .

 let videoData = FileManager.default.contents(atPath: "/path/to/video.mp4")! let imageAttachment = Attachment(data: UIImage(named: "cat")!.jpeg(.high)!, fileName: "cat.jpg", mimeType: .jpg) let payload = PostController.CreateRequest(text: "CodyFire is awesome", tags: ["codyfire", "awesome"], images: [imageAttachment], video: videoData) API.post.create(payload).onProgress { progress in print(" : \(progress)") }.onError { error in print(error.description) }.onSuccess { createdPost in print("  : \(createdPost)") } 

Anhang ist ein Modell, bei dem neben Daten auch der Dateiname und sein MimeType übertragen werden.

Wenn Sie jemals ein mehrteiliges Formular von Swift mit Alamofire oder einer nackten URLRequest eingereicht haben, werden Sie die Einfachheit von CodyFire sicher zu schätzen wissen .

Jetzt einfachere, aber nicht weniger coole Beispiele für GET-Aufrufe.

Post + Get.swift

 extension PostController { struct ListQuery: Codable { let offset, limit: Int init (offset: Int, limit: Int) { self.offset = offset self.limit = limit } } static func get(_ query: ListQuery? = nil) -> APIRequest<[Post]> { return APIRequest("post").query(query) } static func get(id: UUID) -> APIRequest<Post> { return APIRequest("post/" + id.uuidString) } } 

Das einfachste Beispiel ist

 API.post.get(id:) 

In onSuccess wird das Post- Modell an Sie zurückgegeben.

Hier ist ein interessanteres Beispiel.

 API.post.get(PostController.ListQuery(offset: 0, limit: 100)) 

welches ein ListQuery- Modell als Eingabe nimmt,
welche APIRequest schließlich in einen URL-Pfad des Formulars konvertiert

 post?limit=0&offset=100 

und gibt das Array [Post] an onSuccess zurück .

Natürlich können Sie den URL-Pfad auf die alte Art und Weise schreiben, aber jetzt wissen Sie, dass Sie vollständig codierbar sind.

Die letzte Beispielanforderung lautet LÖSCHEN

Post + Delete.swift

 extension PostController { static func delete(id: UUID) -> APIRequest<Nothing> { return APIRequest("post/" + id.uuidString) .method(.delete) .desiredStatusCode(.noContent) } } 

Es gibt zwei interessante Punkte.

- Der Rückgabetyp ist APIRequest. Er gibt den generischen Typ Nothing an , bei dem es sich um ein leeres codierbares Modell handelt.
- Wir haben ausdrücklich darauf hingewiesen, dass wir 204 NO CONTENT erwarten, und CodyFire wird in diesem Fall nur onSuccess aufrufen.

Sie wissen bereits, wie Sie diesen Endpunkt von Ihrem ViewController aus aufrufen.

Es gibt jedoch zwei Optionen: die erste mit onSuccess und die zweite ohne. Wir werden ihn ansehen

 API.post.delete(id:).execute() 

Das heißt, wenn es Ihnen egal ist, ob die Anforderung funktioniert , können Sie einfach .execute () aufrufen und fertig. Andernfalls wird sie nach der onSuccess- Deklaration des Handlers gestartet .

Verfügbare Funktionen


Autorisierung jeder Anfrage


Um jede Anforderungs-API mit beliebigen http-Headern zu signieren, wird ein globaler Handler verwendet, den Sie irgendwo in AppDelegate festlegen können . Darüber hinaus können Sie das klassische [String: String] oder Codable-Modell zur Auswahl verwenden.

Beispiel für Autorisierungsträger.

1. Codierbar (empfehlen)
 CodyFire.shared.fillCodableHeaders = { struct Headers: Codable { //NOTE:  nil,     headers var Authorization: String? var anythingElse: String } return Headers(Authorization: nil, anythingElse: "hello") } 

2. Klassisch [String: String]
 CodyFire.shared.fillHeaders = { guard let apiToken = LocalAuthStorage.savedToken else { return [:] } return ["Authorization": "Bearer \(apiToken)"] } 

Fügen Sie der Anfrage selektiv einige http-Header hinzu


Dies kann beim Erstellen von APIRequest erfolgen, zum Beispiel:

 APIRequest("some/endpoint").headers(["someKey": "someValue"]) 

Bearbeitung nicht autorisierter Anfragen


Sie können sie global verarbeiten, beispielsweise in AppDelegate

 CodyFire.shared.unauthorizedHandler = { //   WelcomeScreen } 

oder lokal in jeder Anfrage

 API.post.create(request).onNotAuthorized { //   } 

Wenn das Netzwerk nicht verfügbar ist


 API.post.create(request). onNetworkUnavailable { //   ,  ,     } 
Andernfalls wird in onError die Fehlermeldung ._notConnectedToInternet angezeigt

Starten Sie etwas, bevor die Anforderung beginnt


Sie können .onRequestStarted festlegen und beispielsweise einen Loader darin anzeigen .
Dies ist ein praktischer Ort, da er bei fehlendem Internet nicht aufgerufen wird und Sie nicht vergeblich erscheinen müssen, um beispielsweise einen Lader anzuzeigen.

So deaktivieren / aktivieren Sie die Protokollausgabe global


 CodyFire.shared.logLevel = .debug CodyFire.shared.logLevel = .error CodyFire.shared.logLevel = .info CodyFire.shared.logLevel = .off 

So deaktivieren Sie die Protokollausgabe für eine einzelne Anforderung


 .avoidLogError() 

Verarbeiten Sie Protokolle auf Ihre eigene Weise


 CodyFire.shared.logHandler = { level, text in print("  CodyFire: " + text) } 

So legen Sie den erwarteten http-Antwortcode des Servers fest


Wie oben erwähnt, erwartet CodyFire standardmäßig 200 OK. Wenn dies der Fall ist, werden Daten analysiert und onSuccess aufgerufen .

Der erwartete Code kann jedoch in Form einer praktischen Aufzählung festgelegt werden, z. B. für 201 CREATED

 .desiredStatusCode(.created) 

oder Sie können sogar benutzerdefinierten erwarteten Code festlegen

 .desiredStatusCode(.custom(777)) 

Anfrage abbrechen


 .cancel() 

und Sie können herausfinden, dass die Anforderung abgebrochen wurde, indem Sie einen .onCancellation- Handler deklarieren

 .onCancellation { //   } 

Andernfalls wird onError ausgelöst

Festlegen eines Zeitlimits für eine Anforderung


 .responseTimeout(30) //   30  

Ein Handler kann auch an ein Timeout-Ereignis gehängt werden

 . onTimeout { //    } 

Andernfalls wird onError ausgelöst

Festlegen eines interaktiven zusätzlichen Zeitlimits


Dies ist meine Lieblingsfunktion. Ein Kunde aus den USA hat mich einmal nach ihr gefragt, weil Es gefiel ihm nicht, dass das Anmeldeformular zu schnell funktionierte. Seiner Meinung nach sah es nicht natürlich aus, als wäre es eine Fälschung, keine Autorisierung.

Die Idee ist, dass die E-Mail- / Passwortprüfung mindestens 2 Sekunden dauern soll. Und wenn es nur 0,5 Sekunden dauert, müssen Sie weitere 1,5 werfen und erst dann onSuccess aufrufen . Und wenn es genau 2 oder 2,5 Sekunden dauert, rufen Sie sofort onSuccess auf .

 .additionalTimeout(2) // 2     

Benutzerdefinierter Datumscodierer / -decodierer


CodyFire verfügt über eine eigene DateCodingStrategy-Enumeration , in der drei Werte enthalten sind

- Sekunden seit 1970
- Millisekunden seit 1970
- formatiert (_ customDateFormatter: DateFormatter)

DateCodingStrategy kann auf drei Arten und separat zum Decodieren und Codieren festgelegt werden
- global in AppDelegate

 CodyFire.shared.dateEncodingStrategy = .secondsSince1970 let customDateFormatter = DateFormatter() CodyFire.shared.dateDecodingStrategy = .formatted(customDateFormatter) 

- für eine Anfrage

 APIRequest("some/endpoint") .dateDecodingStrategy(.millisecondsSince1970) .dateEncodingStrategy(.secondsSince1970) 

- oder sogar separat für jedes Modell, Sie benötigen nur das Modell, das mit CustomDateEncodingStrategy und / oder CustomDateDecodingStrategy übereinstimmt .

 struct SomePayload: JSONPayload, CustomDateEncodingStrategy, CustomDateDecodingStrategy { var dateEncodingStrategy: DateCodingStrategy var dateDecodingStrategy: DateCodingStrategy } 

So fügen Sie dem Projekt hinzu


Die Bibliothek ist auf GitHub unter der MIT-Lizenz verfügbar.

Die Installation ist derzeit nur über CocoaPods möglich
 pod 'CodyFire' 


Ich hoffe wirklich, dass CodyFire für andere iOS-Entwickler nützlich sein wird, die Entwicklung für sie vereinfacht und die Welt im Allgemeinen ein wenig besser und die Menschen freundlicher macht.

Das ist alles, danke für deine Zeit.

UPD: Unterstützung für ReactiveCocoa und RxSwift hinzugefügt
 pod 'ReactiveCodyFire' # ReactiveCocoa pod 'RxCodyFire' # RxSwift #      'CodyFire',     

APIRequest für ReactiveCoca hat .signalProducer und für RxSwift .observable

UPD2: Jetzt können Sie mehrere Anforderungen ausführen
Wenn es wichtig ist, dass Sie das Ergebnis jeder Abfrage erhalten, verwenden Sie .and ()
Maximal in diesem Modus können Sie bis zu 10 Anfragen ausführen, die streng nacheinander ausgeführt werden.
 API.employee.all() .and(API.office.all()) .and(API.car.all()) .and(API.event.all()) .and(API.post.all()) .onError { error in print(error.description) }.onSuccess { employees, offices, cars, events, posts in //    !!! } 

onRequestStarted, onNetworkUnavailable, onCancellation, onNotAuthorized, onTimeout sind ebenfalls verfügbar.
onProgress - noch in der Entwicklung

Wenn Sie sich nicht um die Ergebnisse Ihrer Abfragen kümmern, können Sie .flatten () verwenden.
 [API.employee.all(), API.office.all(), API.car.all()].flatten().onError { print(error.description) }.onSuccess { print("flatten finished!") } 
Um sie gleichzeitig auszuführen, fügen Sie einfach .concurrent (by: 3) hinzu. Dadurch können drei Anforderungen gleichzeitig ausgeführt werden. Es kann eine beliebige Anzahl angegeben werden.
Um fehlgeschlagene Abfragefehler zu überspringen, fügen Sie .avoidCancelOnError () hinzu.
Fügen Sie .onProgress hinzu, um Fortschritte zu erzielen

UPD3: Jetzt können Sie für jede Anforderung einen separaten Server festlegen
Es ist notwendig, die erforderlichen Serveradressen irgendwo zu erstellen, zum Beispiel so
 let server1 = ServerURL(base: "https://server1.com", path: "v1") let server2 = ServerURL(base: "https://server2.com", path: "v1") let server3 = ServerURL(base: "https://server3.com") 
Und jetzt können Sie sie direkt bei der Initialisierung der Anforderung verwenden, bevor Sie den Endpunkt angeben
 APIRequest(server1, "endpoint", payload: payloadObject) APIRequest(server2, "endpoint", payload: payloadObject) APIRequest(server3, "endpoint", payload: payloadObject) 
oder Sie können den Server nach dem Initialisieren der Anforderung angeben
 APIRequest("endpoint", payload: payloadObject).serverURL(server1) 

Source: https://habr.com/ru/post/de427937/


All Articles