Bonjour, Habr!
À partir de Swift 4, nous avons accès au nouveau protocole Codable, qui facilite le codage / décodage des modèles. Mes projets contiennent beaucoup de code pour les appels d'API, et au cours de la dernière année, j'ai fait beaucoup de travail pour optimiser cette énorme gamme de code en quelque chose de très léger, concis et simple en éliminant le code répétitif et en utilisant Codable même pour les demandes en plusieurs parties et les paramètres de requête URL. Il s'est avéré être plusieurs excellentes classes à mon avis pour envoyer des demandes et analyser les réponses du serveur. Ainsi qu'une structure de fichiers pratique, qui est les contrôleurs de chaque groupe de requêtes, que j'ai pris en charge lors de l'utilisation de Vapor 3 sur le backend. Il y a quelques jours, j'ai alloué tous mes développements à une bibliothèque séparée et je l'ai nommée CodyFire. Je voudrais parler d'elle dans cet article.
Clause de non-responsabilité
CodyFire est basé sur Alamofire, mais c'est plus qu'un simple wrapper sur Alamofire, c'est une approche système globale pour travailler avec l'API REST pour iOS. C'est pourquoi je ne m'inquiète pas qu'Alamofire scie la cinquième version dans laquelle il y aura un support Codable, car cela ne tuera pas ma création.
Initialisation
Commençons un peu à distance, à savoir que nous avons souvent trois serveurs:
dev - pour le développement, ce que nous partons de Xcode
étape - pour les tests avant la sortie, généralement dans TestFlight ou InHouse
prod - production, pour l'AppStore
Et de nombreux développeurs iOS, bien sûr, sont conscients de l'existence de
variables d'environnement et des schémas de démarrage dans Xcode, mais au cours de ma pratique (plus de 8 ans), 90% des développeurs écrivent manuellement le bon serveur dans une constante pendant les tests ou avant l'assemblage, et c'est ce que je voudrais corriger en montrant un bon exemple de la façon de le faire correctement.
Par défaut, CodyFire détermine automatiquement dans quel environnement l'application s'exécute actuellement, cela le rend très simple:
#if DEBUG //DEV environment #else if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { //TESTFLIGHT environment } else { //APPSTORE environment } #endif
C'est bien sûr sous le capot, et dans le projet dans AppDelegate, vous n'avez qu'à enregistrer trois URL
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 } }
Et on pourrait simplement s'en réjouir et ne rien faire d'autre.
Mais dans la vraie vie, nous devons souvent tester les serveurs de développement, de scène et de production dans Xcode, et pour cela, je vous invite à utiliser des schémas de démarrage.

Astuce: dans la section Gérer les schémas , n'oubliez pas de cocher la case `shared` pour chaque schéma afin qu'ils soient disponibles pour tous les développeurs du projet.
Dans chaque schéma, vous devez écrire la variable d'environnement `env` qui peut prendre trois valeurs: dev, testFlight, appStore.

Et pour que ces schémas fonctionnent avec CodyFire, vous devez ajouter le code suivant à
AppDelegate.didFinishLaunchingWithOptions après l'initialisation de CodyFire
CodyFire.shared.setupEnvByProjectScheme()
De plus, souvent le patron ou les testeurs de votre projet peuvent vous demander de changer de serveur à la volée quelque part sur
LoginScreen . Avec CodyFire, vous pouvez facilement l'implémenter en commutant le serveur sur une seule ligne, en changeant l'environnement:
CodyFire.shared.environmentMode = .appStore
Cela fonctionnera jusqu'à ce que l'application soit redémarrée, et si vous souhaitez qu'elle soit enregistrée après le lancement, enregistrez la valeur dans UserDefaults , vérifiez quand l'application démarre dans AppDelegate et basculez l'environnement vers celui qui est nécessaire.
J'ai dit ce point important, j'espère qu'il y aura plus de projets dans lesquels le changement d'environnement se fera à merveille. Et en même temps, nous avons déjà initialisé la bibliothèque.
Structure de fichiers et contrôleurs
Vous pouvez maintenant parler de ma vision de la structure des fichiers pour tous les appels d'API, cela peut être appelé l'idéologie de CodyFire.
Voyons à quoi ça ressemble finalement dans le projet

Examinons maintenant les listes de fichiers, commençons par
API.swift .
class API { typealias auth = AuthController typealias post = PostController }
Les liens vers tous les contrôleurs sont répertoriés ici afin qu'ils puissent être facilement appelés via `API.controller.method`.
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") } }
Dans ce décorateur, nous déclarons une fonction pour appeler notre API:
- spécifier le point final
- Méthode HTTP POST
- utiliser un wrapper pour l'authentification de base
- déclarer le texte souhaité pour une réponse spécifique du serveur (c'est pratique)
- et indiquer le modèle par lequel les données seront décodées
Qu'est-ce qui reste caché?
- pas besoin de spécifier l'URL complète du serveur, car il est déjà défini globalement
- n'a pas eu à indiquer que nous nous attendons à recevoir
200 OK si tout va bien
200 OK est le code d'état par défaut attendu par CodyFire pour toutes les demandes, auquel cas les données sont décodées et un rappel est appelé, que tout va bien, voici vos données.
De plus, quelque part dans le code de votre
écran de connexion, vous pouvez simplement appeler
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 et
onSuccess ne sont qu'une petite partie des rappels qu'APIRequest peut retourner, nous en parlerons plus tard.
Dans l'exemple d'entrée, nous n'avons considéré que l'option lorsque les données retournées sont automatiquement décodées, mais vous pouvez dire que vous pourriez vous-même l'implémenter, et vous aurez raison. Par conséquent, considérons la possibilité d'envoyer des données selon le modèle en utilisant le formulaire d'inscription comme exemple.
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") } }
Contrairement à la connexion, lors de l'inscription, nous transmettons une grande quantité de données.
Dans cet exemple, nous avons un modèle
SignupRequest qui est conforme au protocole
JSONPayload (ainsi CodyFire comprend le type de charge utile) de sorte que le corps de notre demande soit sous la forme de JSON. Si vous avez besoin de x-www-form-urlencoded, utilisez alors
FormURLEncodedPayload .
En conséquence, vous obtenez une fonction simple qui accepte le modèle de charge utile
API.auth.signup(request)
et qui, en cas de succès, vous renverra un modèle de réponse spécifique.
Je pense que c'est cool, non?
Mais qu'en est-il si plusieurs parties?
Regardons un exemple où vous pouvez créer un article.
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) } }
Ce code pourra envoyer un formulaire en plusieurs parties avec un tableau de fichiers image et une vidéo.
Voyons comment appeler une dépêche. Voici le moment le plus intéressant de l'
attachement .
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)") }
La pièce jointe est un modèle dans lequel, en plus des
données, le nom de fichier et son MimeType sont également transmis.
Si vous avez déjà soumis un formulaire en plusieurs parties de Swift en utilisant Alamofire ou une
URLRequest nue
, je suis sûr que vous apprécierez la simplicité de
CodyFire .
Maintenant, des exemples d'appels GET plus simples mais non moins cool.
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) } }
L'exemple le plus simple est
API.post.get(id:)
qui dans
onSuccess vous renverra le modèle
Post .
Voici un exemple plus intéressant.
API.post.get(PostController.ListQuery(offset: 0, limit: 100))
qui prend un modèle
ListQuery en entrée,
qu'APIRequest convertit finalement en un chemin URL du formulaire
post?limit=0&offset=100
et renverra le tableau
[Post] à
onSuccess .
Bien sûr, vous pouvez écrire le chemin URL à l'ancienne, mais maintenant vous savez que vous pouvez totalement coder.
La dernière demande d'exemple sera DELETE
Publier + Supprimer.swift extension PostController { static func delete(id: UUID) -> APIRequest<Nothing> { return APIRequest("post/" + id.uuidString) .method(.delete) .desiredStatusCode(.noContent) } }
Il y a deux points intéressants.
- le type de retour est APIRequest, il spécifie le type générique
Nothing , qui est un modèle
Codable vide.
- nous avons explicitement indiqué que nous prévoyons de recevoir 204 AUCUN CONTENU, et CodyFire n'appellera
onSuccès que dans ce cas.
Vous savez déjà comment appeler ce point de terminaison à partir de votre ViewController.
Mais il y a deux options, la première avec
onSuccess et la seconde sans. On va le regarder
API.post.delete(id:).execute()
Autrement dit, si cela n'a pas d'importance pour vous si la demande
fonctionne , vous pouvez simplement appeler
.execute () dessus et c'est tout, sinon elle commencera après la déclaration
onSuccess du gestionnaire.
Fonctions disponibles
Autorisation de chaque demande
Pour signer chaque API de demande avec des en-têtes http, un gestionnaire global est utilisé, que vous pouvez définir quelque part dans
AppDelegate . De plus, vous pouvez utiliser le modèle classique [String: String] ou Codable pour choisir.
Exemple pour le porteur d'autorisation.
1. Codable (recommander)
CodyFire.shared.fillCodableHeaders = { struct Headers: Codable { //NOTE: nil, headers var Authorization: String? var anythingElse: String } return Headers(Authorization: nil, anythingElse: "hello") }
2. Classique [String: String]
CodyFire.shared.fillHeaders = { guard let apiToken = LocalAuthStorage.savedToken else { return [:] } return ["Authorization": "Bearer \(apiToken)"] }
Ajouter sélectivement des en-têtes http à la demande
Cela peut être fait lors de la création d'APIRequest, par exemple:
APIRequest("some/endpoint").headers(["someKey": "someValue"])
Gérer les demandes non autorisées
Vous pouvez les traiter globalement, par exemple dans
AppDelegate CodyFire.shared.unauthorizedHandler = {
ou localement à chaque demande
API.post.create(request).onNotAuthorized {
Si le réseau n'est pas disponible
API.post.create(request). onNetworkUnavailable {
sinon, dans
onError, vous obtenez une erreur
._notConnectedToInternetCommencer quelque chose avant le début de la demande
Vous pouvez définir
.onRequestStarted et commencer à afficher, par exemple, un chargeur dedans.
C'est un endroit pratique, car il n'est pas appelé en cas de manque d'Internet, et vous n'avez pas besoin de vous présenter en vain pour afficher un chargeur, par exemple.
Comment désactiver / activer la sortie du journal globalement
CodyFire.shared.logLevel = .debug CodyFire.shared.logLevel = .error CodyFire.shared.logLevel = .info CodyFire.shared.logLevel = .off
Comment désactiver la sortie du journal pour une seule demande
.avoidLogError()
Traitez les journaux à votre manière
CodyFire.shared.logHandler = { level, text in print(" CodyFire: " + text) }
Comment définir le code de réponse http attendu du serveur
Comme je l'ai dit ci-dessus, par défaut, CodyFire s'attend à recevoir
200 OK, et si c'est le cas, il commence à analyser les données et appelle
onSuccess .
Mais le code attendu peut être défini sous la forme d'une énumération pratique, par exemple, pour
201 CREATED .desiredStatusCode(.created)
ou vous pouvez même définir un code attendu personnalisé
.desiredStatusCode(.custom(777))
Annuler la demande
.cancel()
et vous pouvez découvrir que la demande a été annulée en déclarant un gestionnaire
.onCancellation .onCancellation {
sinon
onError sera
levéDéfinition d'un délai d'expiration pour une demande
.responseTimeout(30)
un gestionnaire peut également être suspendu lors d'un événement de temporisation
. onTimeout {
sinon
onError sera
levéDéfinition d'un délai supplémentaire interactif
Ceci est ma fonctionnalité préférée. Un client des États-Unis m'a un jour interrogé à son sujet, car il n'aimait pas que le formulaire de connexion fonctionne trop rapidement, à son avis, cela n'avait pas l'air naturel, comme s'il s'agissait d'un faux, pas d'une autorisation.
L'idée est qu'il voulait que la vérification de l'e-mail / mot de passe dure au moins 2 secondes. Et si cela ne dure que 0,5 seconde, vous devez lancer 1,5 autre et ensuite appeler
onSuccess . Et si cela prend exactement 2 ou 2,5 secondes, appelez immédiatement
onSuccess .
.additionalTimeout(2)
Encodeur / décodeur de date personnalisé
CodyFire a sa propre énumération
DateCodingStrategy , dans laquelle il existe trois valeurs
- secondesDepuis1970
- millisecondes Depuis 1970
- formaté (_ customDateFormatter: DateFormatter)
DateCodingStrategy peut être défini de trois manières et séparément pour le décodage et l'encodage
- globalement dans
AppDelegate CodyFire.shared.dateEncodingStrategy = .secondsSince1970 let customDateFormatter = DateFormatter() CodyFire.shared.dateDecodingStrategy = .formatted(customDateFormatter)
- pour une demande
APIRequest("some/endpoint") .dateDecodingStrategy(.millisecondsSince1970) .dateEncodingStrategy(.secondsSince1970)
- ou même séparément pour chaque modèle, vous avez juste besoin que le modèle corresponde à
CustomDateEncodingStrategy et / ou
CustomDateDecodingStrategy .
struct SomePayload: JSONPayload, CustomDateEncodingStrategy, CustomDateDecodingStrategy { var dateEncodingStrategy: DateCodingStrategy var dateDecodingStrategy: DateCodingStrategy }
Comment ajouter au projet
La bibliothèque est disponible sur GitHub sous la licence MIT.L'installation n'est actuellement disponible que via CocoaPods
pod 'CodyFire'
J'espère vraiment que CodyFire sera utile à d'autres développeurs iOS, cela simplifiera le développement pour eux et rendra le monde un peu meilleur et les gens plus gentils.
C'est tout, merci pour votre temps.
UPD: prise en charge de ReactiveCocoa et RxSwift pod 'ReactiveCodyFire'
APIRequest pour ReactiveCoca aura
.signalProducer , et pour RxSwift
.observableUPD2: vous pouvez désormais exécuter plusieurs requêtesS'il est important pour vous d'obtenir le résultat de chaque requête, utilisez
.and ()Maximum dans ce mode, vous pouvez exécuter jusqu'à 10 requêtes, elles seront exécutées strictement l'une après l'autre.
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 sont également disponibles.
onProgress - toujours en développement
Si vous ne vous souciez pas des résultats de vos requêtes, vous pouvez utiliser
.flatten () [API.employee.all(), API.office.all(), API.car.all()].flatten().onError { print(error.description) }.onSuccess { print("flatten finished!") }
Pour les exécuter en même temps, ajoutez simplement
.concurrent (par: 3), cela permettra d'exécuter trois requêtes simultanément, n'importe quel nombre peut être spécifié.
Pour ignorer les erreurs de requête ayant échoué, ajoutez
.avoidCancelOnError ()Pour progresser, ajoutez
.onProgressUPD3: vous pouvez maintenant définir un serveur séparé pour chaque demandeIl est nécessaire de créer les adresses de serveur nécessaires quelque part, par exemple comme ceci
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")
Et maintenant, vous pouvez les utiliser directement dans l'initialisation de la demande avant de spécifier le point de terminaison
APIRequest(server1, "endpoint", payload: payloadObject) APIRequest(server2, "endpoint", payload: payloadObject) APIRequest(server3, "endpoint", payload: payloadObject)
ou vous pouvez spécifier le serveur après avoir initialisé la demande
APIRequest("endpoint", payload: payloadObject).serverURL(server1)