Codable pour les requêtes API et comment mettre le code en ordre

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.

image
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.

image

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

image

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 = { //   WelcomeScreen } 

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 ._notConnectedToInternet

Commencer 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) //   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) // 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' # ReactiveCocoa pod 'RxCodyFire' # RxSwift #      'CodyFire',     

APIRequest pour ReactiveCoca aura .signalProducer , et pour RxSwift .observable

UPD2: vous pouvez désormais exécuter plusieurs requêtes
S'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 .onProgress

UPD3: vous pouvez maintenant définir un serveur séparé pour chaque demande
Il 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) 

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


All Articles