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)