Écrire votre couche réseau dans Swift: approche orientée protocole



Désormais, près de 100% des applications utilisent la mise en réseau, tout le monde est donc confronté à l'organisation et à l'utilisation de la couche réseau. Il existe deux approches principales pour résoudre ce problème, soit en utilisant des bibliothèques tierces, soit votre propre implémentation de la couche réseau. Dans cet article, nous considérerons la deuxième option et essayerons d'implémenter une couche réseau en utilisant toutes les dernières fonctionnalités du langage, en utilisant des protocoles et des énumérations. Cela sauvera le projet des dépendances inutiles sous la forme de bibliothèques supplémentaires. Ceux qui ont déjà vu Moya reconnaîtront immédiatement beaucoup de détails similaires dans la mise en œuvre et l'utilisation, tels qu'ils sont, mais cette fois-ci, nous le ferons nous-mêmes sans toucher Moya et Alamofire.


Dans ce guide, nous verrons comment implémenter une couche réseau sur Swift pur, sans utiliser de bibliothèques tierces. Une fois que vous aurez lu cet article, votre code deviendra

  • orienté protocole
  • facile à utiliser
  • facile à utiliser
  • type sûr
  • pour les points d'extrémité, les énumérations seront utilisées


Voici un exemple de la façon dont l'utilisation de notre couche réseau se chargera de sa mise en œuvre:



En écrivant simplement router.request (. Et en utilisant toute la puissance des énumérations, nous verrons toutes les options de requête possibles et leurs paramètres.

Tout d'abord, un peu sur la structure du projet

Chaque fois que vous créez quelque chose de nouveau et afin de pouvoir tout comprendre facilement à l'avenir, il est très important de tout organiser et structurer correctement. Je pense qu'une structure de dossiers correctement organisée est un détail important lors de la construction de l'architecture de l'application. Pour que tout soit correctement organisé dans des dossiers, créons-les à l'avance. Cela ressemblera à la structure générale des dossiers dans le projet:



Protocole Endpointtype

Tout d'abord, nous devons définir notre protocole EndPointType . Ce protocole contiendra toutes les informations nécessaires pour configurer la demande. Qu'est-ce qu'une demande (endpoint)? En substance, il s'agit d'une URLRequest avec tous les composants associés, tels que les en-têtes, les paramètres de demande et le corps de la demande. Le protocole EndPointType est la partie la plus importante de notre implémentation de la couche réseau. Créons un fichier et nommez-le EndPointType . Mettez ce fichier dans le dossier Service (pas dans le dossier EndPoint, pourquoi - il sera clair un peu plus tard)



Protocoles HTTP

Notre EndPointType contient plusieurs protocoles dont nous avons besoin pour créer une demande. Voyons quels sont ces protocoles.

HTTPMethod

Créez un fichier, nommez-le HTTPMethod et placez-le dans le dossier Service. Cette liste sera utilisée pour définir la méthode HTTP de notre demande.



HTTPTask
Créez un fichier, nommez-le HTTPTask et placez-le dans le dossier Service. HTTPTask est responsable de la configuration des paramètres d'une demande spécifique. Vous pouvez y ajouter autant d'options de requête que vous le souhaitez, mais je vais à mon tour effectuer des requêtes régulières, des requêtes avec des paramètres, des requêtes avec des paramètres et des en-têtes, donc je ne ferai que ces trois types de requêtes.



Dans la section suivante, nous discuterons des paramètres et de la façon dont nous travaillerons avec eux

HTTPHeaders

Les HTTPHeaders ne sont que des typealias pour un dictionnaire. Vous pouvez le créer en haut de votre fichier HTTPTask .

public typealias HTTPHeaders = [String:String] 


Paramètres et encodage

Créez un fichier, nommez-le ParameterEncoding et placez-le dans le dossier Encoding. Créez des typealias pour les paramètres , ce sera à nouveau un dictionnaire normal. Nous faisons cela pour rendre le code plus compréhensible et lisible.

 public typealias Parameters = [String:Any] 


Ensuite, définissez un protocole ParameterEncoder avec une seule fonction de codage. La méthode d'encodage a deux paramètres: inout URLRequest et Parameters . INOUT est un mot clé Swift qui définit un paramètre de fonction comme référence. En règle générale, les paramètres sont transmis à la fonction sous forme de valeurs. Lorsque vous écrivez inout avant un paramètre de fonction dans un appel, vous définissez ce paramètre comme type de référence. Pour en savoir plus sur les arguments inout, vous pouvez suivre ce lien. En bref, inout vous permet de modifier la valeur de la variable elle-même, qui a été transmise à la fonction, et pas seulement d'obtenir sa valeur dans le paramètre et de travailler avec elle à l'intérieur de la fonction. Le protocole ParameterEncoder sera implémenté dans JSONParameterEncoder et dans URLPameterEncoder .

 public protocol ParameterEncoder { static func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws } 


ParameterEncoder contient une seule fonction dont la tâche est de coder les paramètres. Cette méthode peut renvoyer une erreur qui doit être gérée, nous utilisons donc throw.

Il peut également être utile de produire non pas des erreurs standard, mais des erreurs personnalisées. Il est toujours assez difficile de décrypter ce que Xcode vous donne. Lorsque vous avez personnalisé et décrit toutes les erreurs, vous savez toujours exactement ce qui s'est passé. Pour ce faire, définissons une énumération qui hérite de Error .



Créez un fichier, nommez-le URLParameterEncoder et placez-le dans le dossier Encoding .



Ce code prend une liste de paramètres, les convertit et les met en forme pour les utiliser comme paramètres d'URL. Comme vous le savez, certains caractères ne sont pas autorisés dans l'URL. Les paramètres sont également séparés par le symbole "&", nous devons donc prendre soin de cela. Nous devons également définir la valeur par défaut pour les en-têtes s'ils ne sont pas définis dans la demande.

C'est la partie du code qui est censée être couverte par les tests unitaires. La construction d'une demande d'URL est la clé, sinon nous pouvons provoquer de nombreuses erreurs inutiles. Si vous utilisez l'API ouverte, vous ne voudrez évidemment pas utiliser tout le volume possible de demandes d'échecs de tests. Si vous voulez en savoir plus sur les tests unitaires, vous pouvez commencer par cet article.

JSONParameterEncoder

Créez un fichier, nommez-le JSONParameterEncoder et placez-le dans le dossier Encoding.



Tout est le même que dans le cas de URLParameter , juste ici nous allons convertir les paramètres pour JSON et ajouter à nouveau les paramètres définissant le codage "application / json" à l'en-tête.

Routeur réseau

Créez un fichier, nommez-le NetworkRouter et placez-le dans le dossier Service. Commençons par définir les typealias pour la fermeture.

 public typealias NetworkRouterCompletion = (_ data: Data?,_ response: URLResponse?,_ error: Error?)->() 


Ensuite, nous définissons le protocole NetworkRouter .



NetworkRouter a un EndPoint qu'il utilise pour les demandes et dès que la demande est terminée, le résultat de cette demande est transmis à la fermeture de NetworkRouterCompletion . Le protocole a également une fonction d' annulation , qui peut être utilisée pour interrompre les demandes de chargement et de déchargement à long terme. Nous avons également utilisé le type associé ici parce que nous voulons que notre routeur prenne en charge tout type de EndPointType . Sans utiliser le type associé, le routeur devrait avoir un type spécifique qui implémente EndPointType . Si vous souhaitez en savoir plus sur le type associé, vous pouvez lire cet article .

Routeur

Créez un fichier, nommez-le Routeur et placez-le dans le dossier Service. Nous déclarons une variable privée de type URLSessionTask . Tout le travail y sera. Nous le rendons privé parce que nous ne voulons pas que quelqu'un à l'extérieur puisse le changer.



Demande

Ici, nous créons URLSession en utilisant URLSession.shared , c'est la façon la plus simple de créer. Mais rappelez-vous que cette méthode n'est pas la seule. Vous pouvez utiliser des configurations URLSession plus complexes qui peuvent modifier son comportement. Plus d'informations à ce sujet dans cet article .

La demande est créée en appelant la fonction buildRequest. L'appel de fonction est encapsulé dans do-try-catch, car les fonctions de codage à l'intérieur de buildRequest peuvent lever des exceptions. La réponse , les données et l' erreur sont transmises à la fin.



Demande de build

Nous créons notre requête en utilisant la fonction buildRequest . Cette fonction est responsable de tout le travail essentiel dans notre couche réseau. Convertit essentiellement EndPointType en URLRequest . Et dès que EndPoint se transforme en demande, nous pouvons la transmettre à la session . Beaucoup de choses se passent ici, alors regardons les méthodes. Examinons d' abord la méthode buildRequest :

1. Nous initialisons la variable de demande URLRequest . Nous y définissons notre URL de base et y ajoutons le chemin de la requête spécifique qui sera utilisée.

2. Attribuez à request.httpMethod la méthode http de notre EndPoint .

3. Nous créons un bloc do-try-catch, car nos encodeurs peuvent générer une erreur. En créant un grand bloc do-try-catch, nous éliminons la nécessité de créer un bloc distinct pour chaque essai.

4. Dans switch, vérifiez route.task .

5. Selon le type de tâche, nous appelons l'encodeur correspondant.



Configurer les paramètres

Créez la fonction configureParameters dans le routeur.



Cette fonction est responsable de la conversion de nos paramètres de requête. Étant donné que notre API suppose l'utilisation de bodyParameters sous la forme de JSON et d' URLParameters convertis au format URL, nous passons simplement les paramètres appropriés aux fonctions de conversion correspondantes, que nous avons décrites au début de l'article. Si vous utilisez une API qui inclut différents types d'encodages, dans ce cas, je recommanderais d'ajouter HTTPTask avec une énumération supplémentaire avec le type d'encodage. Cette liste doit contenir tous les types d'encodages possibles. Après cela, dans configureParameters, ajoutez un argument supplémentaire avec cette énumération. Selon sa valeur, changez en utilisant switch et faites l'encodage dont vous avez besoin.

Ajouter des en-têtes supplémentaires

Créez la fonction addAdditionalHeaders dans le routeur.



Ajoutez simplement tous les en-têtes nécessaires à la demande.

Annuler

La fonction d' annulation sera assez simple:



Exemple d'utilisation

Essayons maintenant d'utiliser notre couche réseau sur un exemple réel. Nous nous connecterons à TheMovieDB pour recevoir les données de notre application.

MovieEndPoint

Créez un fichier MovieEndPoint et placez-le dans le dossier EndPoint. MovieEndPoint est le même que
et TargetType dans Moya. Ici, nous implémentons notre propre EndPointType à la place. Un article décrivant comment utiliser Moya pour un exemple similaire peut être trouvé sur ce lien .

 import Foundation enum NetworkEnvironment { case qa case production case staging } public enum MovieApi { case recommended(id:Int) case popular(page:Int) case newMovies(page:Int) case video(id:Int) } extension MovieApi: EndPointType { var environmentBaseURL : String { switch NetworkManager.environment { case .production: return "https://api.themoviedb.org/3/movie/" case .qa: return "https://qa.themoviedb.org/3/movie/" case .staging: return "https://staging.themoviedb.org/3/movie/" } } var baseURL: URL { guard let url = URL(string: environmentBaseURL) else { fatalError("baseURL could not be configured.")} return url } var path: String { switch self { case .recommended(let id): return "\(id)/recommendations" case .popular: return "popular" case .newMovies: return "now_playing" case .video(let id): return "\(id)/videos" } } var httpMethod: HTTPMethod { return .get } var task: HTTPTask { switch self { case .newMovies(let page): return .requestParameters(bodyParameters: nil, urlParameters: ["page":page, "api_key":NetworkManager.MovieAPIKey]) default: return .request } } var headers: HTTPHeaders? { return nil } } 


Moviemodel

Pour analyser le modèle de données MovieModel et JSON dans le modèle, le protocole Decodable est utilisé. Placez ce fichier dans le dossier Model .

Remarque : pour une connaissance plus détaillée des protocoles codables, décodables et codables, vous pouvez lire mon autre article , qui décrit en détail toutes les fonctionnalités de leur utilisation.

 import Foundation struct MovieApiResponse { let page: Int let numberOfResults: Int let numberOfPages: Int let movies: [Movie] } extension MovieApiResponse: Decodable { private enum MovieApiResponseCodingKeys: String, CodingKey { case page case numberOfResults = "total_results" case numberOfPages = "total_pages" case movies = "results" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: MovieApiResponseCodingKeys.self) page = try container.decode(Int.self, forKey: .page) numberOfResults = try container.decode(Int.self, forKey: .numberOfResults) numberOfPages = try container.decode(Int.self, forKey: .numberOfPages) movies = try container.decode([Movie].self, forKey: .movies) } } struct Movie { let id: Int let posterPath: String let backdrop: String let title: String let releaseDate: String let rating: Double let overview: String } extension Movie: Decodable { enum MovieCodingKeys: String, CodingKey { case id case posterPath = "poster_path" case backdrop = "backdrop_path" case title case releaseDate = "release_date" case rating = "vote_average" case overview } init(from decoder: Decoder) throws { let movieContainer = try decoder.container(keyedBy: MovieCodingKeys.self) id = try movieContainer.decode(Int.self, forKey: .id) posterPath = try movieContainer.decode(String.self, forKey: .posterPath) backdrop = try movieContainer.decode(String.self, forKey: .backdrop) title = try movieContainer.decode(String.self, forKey: .title) releaseDate = try movieContainer.decode(String.self, forKey: .releaseDate) rating = try movieContainer.decode(Double.self, forKey: .rating) overview = try movieContainer.decode(String.self, forKey: .overview) } } 


Networkmanager

Créez un fichier NetworkManager dans le dossier Manager. Pour le moment, NetworkManager ne contient que deux propriétés statiques: une clé API et une énumération qui décrit le type de serveur auquel se connecter. NetworkManager contient également un routeur de type MovieApi .



Réponse du réseau

Créez l'énumération NetworkResponse dans NetworkManager.



Nous utilisons cette énumération lors du traitement des réponses aux demandes et nous afficherons le message correspondant.

Résultat

Créez une énumération de résultats dans NetworkManager.



Nous utilisons Result pour déterminer si la demande a réussi ou non. Sinon, nous retournerons un message d'erreur avec la raison.

Traitement des réponses aux demandes

Créez la fonction handleNetworkResponse . Cette fonction prend un argument, tel qu'une réponse HTTP, et renvoie un résultat.



Dans cette fonction, en fonction du statusCode reçu de HTTPResponse, nous renvoyons un message d'erreur ou le signe d'une requête réussie. En règle générale, un code compris entre 200 et 299 signifie succès.

Faire une demande de réseau

Donc, nous avons tout fait pour commencer à utiliser notre couche réseau, essayons de faire une demande.

Nous demanderons une liste de nouveaux films. Créez une fonction et nommez-la getNewMovies .



Prenons-le étape par étape:

1. Nous définissons la méthode getNewMovies avec deux arguments: le numéro de page de pagination et le gestionnaire d'achèvement, qui renvoie un tableau facultatif de modèles Movie , ou une erreur facultative.

2. Appelez le routeur . Nous transmettons le numéro de page et l' achèvement du processus à la fermeture.

3. URLSession renvoie une erreur s'il n'y a pas de réseau ou s'il n'a pas été possible de faire une demande pour une raison quelconque. Veuillez noter qu'il ne s'agit pas d'une erreur d'API, de telles erreurs se produisent sur le client et se produisent généralement en raison de la mauvaise qualité de la connexion Internet.

4. Nous devons convertir notre réponse en HTTPURLResponse , car nous devons accéder à la propriété statusCode .

5. Déclarez le résultat et initialisez-le à l'aide de la méthode handleNetworkResponse

6. Le succès signifie que la demande a été acceptée et nous avons reçu la réponse attendue. Ensuite, nous vérifions si les données sont fournies avec la réponse, et sinon, nous terminons simplement la méthode via return.

7. Si la réponse est accompagnée de données, il est nécessaire d'analyser les données reçues dans le modèle. Après cela, nous passons le tableau de modèles résultant à son terme.

8. En cas d'erreur, transmettez simplement l'erreur à la fin .

Voilà, c'est ainsi que notre propre couche réseau fonctionne sur Swift pur, sans utiliser de dépendances sous forme de pods et de bibliothèques tiers. Afin de faire une demande d'api de test pour obtenir une liste de films, créez un MainViewController avec la propriété NetworkManager et appelez la méthode getNewMovies à travers elle.

  class MainViewController: UIViewController { var networkManager: NetworkManager! init(networkManager: NetworkManager) { super.init(nibName: nil, bundle: nil) self.networkManager = networkManager } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .green networkManager.getNewMovies(page: 1) { movies, error in if let error = error { print(error) } if let movies = movies { print(movies) } } } } 


Petit bonus

Vous avez eu des situations dans Xcode lorsque vous ne compreniez pas quel type d'espace réservé est utilisé dans un endroit particulier? Par exemple, regardez le code que nous venons d'écrire pour Router .



Nous avons déterminé le NetworkRouterCompletion nous-mêmes, mais même dans ce cas, il est facile d'oublier de quel type il s'agit et comment l'utiliser. Mais notre bien-aimé Xcode s'est occupé de tout, et il suffit de double-cliquer sur l'espace réservé et Xcode remplacera le type souhaité.



Conclusion

Nous avons maintenant une implémentation d'une couche réseau orientée protocole, qui est très facile à utiliser et qui peut toujours être personnalisée selon vos besoins. Nous avons compris sa fonctionnalité et le fonctionnement de tous les mécanismes.

Vous pouvez trouver le code source dans ce référentiel .

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


All Articles