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.

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.

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

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 = {
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 angezeigtStarten 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östFestlegen eines Zeitlimits für eine Anforderung
.responseTimeout(30)
Ein Handler kann auch an ein Timeout-Ereignis gehängt werden
. onTimeout {
Andernfalls wird
onError ausgelöstFestlegen 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)
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'
APIRequest für ReactiveCoca hat
.signalProducer und für RxSwift
.observableUPD2: Jetzt können Sie mehrere Anforderungen ausführenWenn 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
erzielenUPD3: Jetzt können Sie für jede Anforderung einen separaten Server festlegenEs 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)