
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 projetChaque 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 EndpointtypeTout 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 HTTPNotre
EndPointType contient plusieurs protocoles dont nous avons besoin pour créer une demande. Voyons quels sont ces protocoles.
HTTPMethodCré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.
HTTPTaskCré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
HTTPHeadersLes 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 encodageCré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.
JSONParameterEncoderCré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éseauCré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 .
RouteurCré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.
DemandeIci, 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 buildNous 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ètresCré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émentairesCréez la fonction
addAdditionalHeaders dans le routeur.

Ajoutez simplement tous les en-têtes nécessaires à la demande.
AnnulerLa fonction d'
annulation sera assez simple:
Exemple d'utilisationEssayons maintenant d'utiliser notre couche réseau sur un exemple réel. Nous nous connecterons à
TheMovieDB pour recevoir les données de notre application.
MovieEndPointCré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:
MoviemodelPour 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) } }
NetworkmanagerCré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éseauCré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ésultatCré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 demandesCré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éseauDonc, 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
handleNetworkResponse6.
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 bonusVous 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é.
ConclusionNous 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 .