Mise en réseau iOS centralisée et découplée : Tutoriel AFNetworking avec une classe Singleton
Publié: 2022-03-11En ce qui concerne les modèles d'architecture iOS, le modèle de conception Modèle-Vue-Contrôleur (MVC) est idéal pour la longévité et la maintenabilité de la base de code d'une application. Il permet aux classes d'être facilement réutilisées ou remplacées pour répondre à diverses exigences en les dissociant les unes des autres. Cela permet de maximiser les avantages de la programmation orientée objet (POO).
Bien que cette architecture d'application iOS fonctionne bien au niveau micro (écrans individuels/sections d'une application), vous pouvez vous retrouver à ajouter des fonctions similaires à plusieurs modèles à mesure que votre application se développe. Dans des cas tels que la mise en réseau, le déplacement de la logique commune hors de vos classes de modèle vers des classes d'assistance singleton peut être une meilleure approche. Dans ce didacticiel AFNetworking iOS, je vais vous apprendre à configurer un objet réseau singleton centralisé qui, découplé des composants MVC au niveau micro, peut être réutilisé dans votre application d'architecture découplée.
Le problème avec le réseau iOS
Apple a fait un excellent travail pour résumer de nombreuses complexités de la gestion du matériel mobile dans des SDK iOS faciles à utiliser, mais dans certains cas, tels que la mise en réseau, Bluetooth, OpenGL et le traitement multimédia, les classes peuvent être fastidieuses en raison de leur objectif de garder les SDK flexibles. Heureusement, la riche communauté de développeurs iOS a créé des cadres de haut niveau pour simplifier les cas d'utilisation les plus courants dans le but de simplifier la conception et la structure des applications. Un bon programmeur, utilisant les meilleures pratiques d'architecture d'application ios, sait quels outils utiliser, pourquoi les utiliser et quand il est préférable d'écrire vos propres outils et classes à partir de zéro.
AFNetworking est un excellent exemple de mise en réseau et l'un des frameworks open source les plus couramment utilisés, ce qui simplifie les tâches quotidiennes d'un développeur. Il simplifie la mise en réseau de l'API RESTful et crée des modèles de requête/réponse modulaires avec des blocs d'achèvement de réussite, de progression et d'échec. Cela élimine le besoin de méthodes déléguées implémentées par le développeur et de paramètres de demande/connexion personnalisés et peut être inclus très rapidement dans n'importe quelle classe.
Le problème avec AFNetworking
AFNetworking est formidable, mais sa modularité peut également conduire à son utilisation de manière fragmentée. Les implémentations inefficaces courantes peuvent inclure :
Plusieurs requêtes réseau utilisant des méthodes et des propriétés similaires dans un seul contrôleur de vue
Requêtes presque identiques dans plusieurs contrôleurs de vue qui conduisent à des variables communes distribuées qui peuvent se désynchroniser
Requêtes réseau dans une classe pour des données non liées à cette classe
Pour les applications avec un nombre limité de vues, peu d'appels d'API à implémenter et celles qui ne sont pas susceptibles de changer souvent, cela peut ne pas être très préoccupant. Cependant, il est plus probable que vous voyiez grand et que vous ayez prévu de nombreuses années de mises à jour. Si votre cas est le dernier, vous devrez probablement gérer :
Gestion des versions d'API pour prendre en charge plusieurs générations d'une application
Ajout de nouveaux paramètres ou modifications des paramètres existants au fil du temps pour étendre les capacités
Implémentation d'API totalement nouvelles
Si votre code réseau est dispersé dans toute votre base de code, c'est maintenant un cauchemar potentiel. Avec un peu de chance, vous avez au moins certains de vos paramètres définis statiquement dans un en-tête commun, mais même dans ce cas, vous pouvez toucher une douzaine de classes, même pour les modifications les plus mineures.
Comment traitons-nous les limites d'AFNetworking ?
Créez un singleton réseau pour centraliser la gestion des demandes, des réponses et de leurs paramètres.
Un objet singleton fournit un point d'accès global aux ressources de sa classe. Les singletons sont utilisés dans des situations où ce point de contrôle unique est souhaitable, comme avec des classes qui offrent un service ou une ressource générale. Vous obtenez l'instance globale à partir d'une classe singleton via une méthode de fabrique. - Pomme
Ainsi, un singleton est une classe dont vous n'auriez jamais qu'une seule instance dans une application qui existe pour la durée de vie de l'application. De plus, comme nous savons qu'il n'y a qu'une seule instance, elle est facilement accessible par toute autre classe qui a besoin d'accéder à ses méthodes ou propriétés.
Voici pourquoi nous devrions utiliser un singleton pour la mise en réseau :
Il est initialisé statiquement donc, une fois créé, il aura les mêmes méthodes et propriétés disponibles pour toute classe qui tentera d'y accéder. Il n'y a aucune chance pour des problèmes de synchronisation étranges ou pour demander des données à la mauvaise instance d'une classe.
Vous pouvez limiter vos appels API pour rester sous une limite de débit (par exemple, lorsque vous devez maintenir vos requêtes API à moins de cinq par seconde).
Les propriétés statiques telles que le nom d'hôte, les numéros de port, les points de terminaison, la version de l'API, le type de périphérique, les identifiants persistants, la taille de l'écran, etc. peuvent être colocalisées afin qu'un seul changement affecte toutes les demandes réseau.
Les propriétés communes peuvent être réutilisées entre de nombreuses requêtes réseau.
L'objet singleton n'occupe pas de mémoire tant qu'il n'est pas instancié. Cela peut être utile pour les singletons avec des cas d'utilisation très spécifiques dont certains utilisateurs n'auront peut-être jamais besoin, comme la gestion de la diffusion de vidéo sur un Chromecast s'ils n'ont pas l'appareil.
Les requêtes réseau peuvent être complètement découplées des vues et des contrôleurs afin qu'elles puissent continuer même après la destruction des vues et des contrôleurs.
La journalisation réseau peut être centralisée et simplifiée.
Les événements courants d'échec tels que les alertes peuvent être réutilisés pour toutes les demandes.
La structure principale d'un tel singleton pourrait être réutilisée sur plusieurs projets avec de simples modifications de propriétés statiques de haut niveau.
Quelques raisons de ne pas utiliser de singletons :
Ils peuvent être surutilisés pour fournir plusieurs responsabilités dans une seule classe. Par exemple, les méthodes de traitement vidéo pourraient être mélangées avec des méthodes de mise en réseau ou des méthodes d'état utilisateur. Ce serait probablement une mauvaise pratique de conception et conduirait à un code difficile à comprendre. Au lieu de cela, plusieurs singletons avec des responsabilités spécifiques doivent être créés.
Les singletons ne peuvent pas être sous-classés.
Les singletons peuvent masquer des dépendances et ainsi devenir moins modulaires. Par exemple, si un singleton est supprimé et qu'il manque à une classe une importation importée par le singleton, cela peut entraîner des problèmes futurs (surtout s'il existe des dépendances de bibliothèque externes).
Une classe peut modifier des propriétés partagées dans des singletons lors de longues opérations inattendues dans une autre classe. Sans une réflexion approfondie, les résultats peuvent varier.
Les fuites de mémoire dans un singleton peuvent devenir un problème important puisque le singleton lui-même n'est jamais désalloué.
Cependant, en utilisant les meilleures pratiques d'architecture d'application iOS, ces inconvénients peuvent être atténués. Voici quelques bonnes pratiques :
Chaque singleton doit gérer une seule responsabilité.
N'utilisez pas de singletons pour stocker des données qui seront rapidement modifiées par plusieurs classes ou threads si vous avez besoin d'une grande précision.
Créez des singletons pour activer/désactiver les fonctionnalités en fonction des dépendances disponibles.
Ne stockez pas de grandes quantités de données dans des propriétés singleton car elles persisteront pendant toute la durée de vie de votre application (sauf si elles sont gérées manuellement).
Un exemple simple de singleton avec AFNetworking
Tout d'abord, comme prérequis, ajoutez AFNetworking à votre projet. L'approche la plus simple est via Cocoapods et les instructions se trouvent sur sa page GitHub.
Pendant que vous y êtes, je suggérerais d'ajouter UIAlertController+Blocks
et MBProgressHUD
(encore une fois facilement ajouté avec CocoaPods). Celles-ci sont clairement facultatives, mais cela simplifiera grandement la progression et les alertes si vous souhaitez les implémenter dans le singleton de la fenêtre AppDelegate.
Une fois AFNetworking
ajouté, commencez par créer une nouvelle classe Cocoa Touch appelée NetworkManager
en tant que sous-classe de NSObject
. Ajoutez une méthode de classe pour accéder au gestionnaire. Votre fichier NetworkManager.h
devrait ressembler au code ci-dessous :
#import <Foundation/Foundation.h> #import “AFNetworking.h” @interface NetworkManager : NSObject + (id)sharedManager; @end
Ensuite, implémentez les méthodes d'initialisation de base pour le singleton et importez l'en-tête AFNetworking. Votre implémentation de classe devrait ressembler à ce qui suit (REMARQUE : cela suppose que vous utilisez le comptage automatique des références) :
#import "NetworkManager.h" @interface NetworkManager() @end @implementation NetworkManager #pragma mark - #pragma mark Constructors static NetworkManager *sharedManager = nil; + (NetworkManager*)sharedManager { static dispatch_once_t once; dispatch_once(&once, ^ { sharedManager = [[NetworkManager alloc] init]; }); return sharedManager; } - (id)init { if ((self = [super init])) { } return self; } @end
Génial! Maintenant, nous cuisinons et sommes prêts à ajouter des propriétés et des méthodes. Comme test rapide pour comprendre comment accéder à un singleton, ajoutons ce qui suit à NetworkManager.h
:
@property NSString *appID; - (void)test;
Et ce qui suit à NetworkManager.m
:
#define HOST @”http://www.apitesting.dev/” static const in port = 80; … @implementation NetworkManager … //Set an initial property to init: - (id)init { if ((self = [super init])) { self.appID = @”1”; } return self; } - (void)test { NSLog(@”Testing out the networking singleton for appID: %@, HOST: %@, and PORT: %d”, self.appID, HOST, port); }
Ensuite, dans notre fichier principal ViewController.m
(ou tout ce que vous avez), importez NetworkManager.h
puis dans viewDidLoad
ajoutez :
[[NetworkManager sharedManager] test];
Lancez l'application et vous devriez voir ce qui suit dans le résultat :
Testing our the networking singleton for appID: 1, HOST: http://www.apitesting.dev/, and PORT: 80
Ok, donc vous ne mélangerez probablement pas #define
, static const et @property
en même temps comme ceci, mais simplement en montrant pour plus de clarté vos options. "static const" est une meilleure déclaration pour la sécurité du type mais #define
peut être utile dans la construction de chaînes car il permet d'utiliser des macros. Pour ce que ça vaut, j'utilise #define
pour plus de brièveté dans ce scénario. À moins que vous n'utilisiez des pointeurs, il n'y a pas beaucoup de différence en pratique entre ces approches de déclaration.

Maintenant que vous comprenez #defines
, les constantes, les propriétés et les méthodes, nous pouvons les supprimer et passer à des exemples plus pertinents.
Un exemple de mise en réseau
Imaginez une application où l'utilisateur doit être connecté pour accéder à quoi que ce soit. Lors du lancement de l'application, nous vérifierons si nous avons enregistré un jeton d'authentification et, si c'est le cas, effectuerons une requête GET à notre API pour voir si le jeton a expiré ou non.
Dans AppDelegate.m
, enregistrons une valeur par défaut pour notre jeton :
+ (void)initialize { NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:@"", @"token", nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; }
Nous ajouterons une vérification de jeton au NetworkManager et obtiendrons des commentaires sur la vérification via des blocs d'achèvement. Vous pouvez concevoir ces blocs de complétion comme bon vous semble. Dans cet exemple, j'utilise le succès avec les données de l'objet de réponse et l'échec avec la chaîne de réponse d'erreur et un code d'état. Remarque : L'échec peut éventuellement être omis s'il n'a pas d'importance pour le côté récepteur, comme l'incrémentation d'une valeur dans l'analyse.
NetworkManager.h
Ci-dessus @interface
:
typedef void (^NetworkManagerSuccess)(id responseObject); typedef void (^NetworkManagerFailure)(NSString *failureReason, NSInteger statusCode);
Dans @interface :
@property (non atomique, fort) AFHTTPSessionManager *networkingManager;
- (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;
NetworkManager.m :
Définissez notre BASE_URL :
#define ENABLE_SSL 1 #define HOST @"http://www.apitesting.dev/" #define PROTOCOL (ENABLE_SSL ? @"https://" : @"http://") #define PORT @"80" #define BASE_URL [NSString stringWithFormat:@"%@%@:%@", PROTOCOL, HOST, PORT]
Nous allons ajouter quelques méthodes d'assistance pour simplifier les requêtes authentifiées ainsi que les erreurs d'analyse (cet exemple utilise un jeton Web JSON) :
- (AFHTTPSessionManager*)getNetworkingManagerWithToken:(NSString*)token { if (self.networkingManager == nil) { self.networkingManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:BASE_URL]]; if (token != nil && [token length] > 0) { NSString *headerToken = [NSString stringWithFormat:@"%@ %@", @"JWT", token]; [self.networkingManager.requestSerializer setValue:headerToken forHTTPHeaderField:@"Authorization"]; // Example - [networkingManager.requestSerializer setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; } self.networkingManager.requestSerializer = [AFJSONRequestSerializer serializer]; self.networkingManager.responseSerializer.acceptableContentTypes = [self.networkingManager.responseSerializer.acceptableContentTypes setByAddingObjectsFromArray:@[@"text/html", @"application/json", @"text/json"]]; self.networkingManager.securityPolicy = [self getSecurityPolicy]; } return self.networkingManager; } - (id)getSecurityPolicy { return [AFSecurityPolicy defaultPolicy]; /* Example - AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone]; [policy setAllowInvalidCertificates:YES]; [policy setValidatesDomainName:NO]; return policy; */ } - (NSString*)getError:(NSError*)error { if (error != nil) { NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; NSDictionary *responseObject = [NSJSONSerialization JSONObjectWithData: errorData options:kNilOptions error:nil]; if (responseObject != nil && [responseObject isKindOfClass:[NSDictionary class]] && [responseObject objectForKey:@"message"] != nil && [[responseObject objectForKey:@"message"] length] > 0) { return [responseObject objectForKey:@"message"]; } } return @"Server Error. Please try again later"; }
Si vous avez ajouté MBProgressHUD, il peut être utilisé ici :
#import "MBProgressHUD.h" @interface NetworkManager() @property (nonatomic, strong) MBProgressHUD *progressHUD; @end … - (void)showProgressHUD { [self hideProgressHUD]; self.progressHUD = [MBProgressHUD showHUDAddedTo:[[UIApplication sharedApplication] delegate].window animated:YES]; [self.progressHUD removeFromSuperViewOnHide]; self.progressHUD.bezelView.color = [UIColor colorWithWhite:0.0 alpha:1.0]; self.progressHUD.contentColor = [UIColor whiteColor]; } - (void)hideProgressHUD { if (self.progressHUD != nil) { [self.progressHUD hideAnimated:YES]; [self.progressHUD removeFromSuperview]; self.progressHUD = nil; } }
Et notre demande de vérification de jeton :
- (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSString *token = [defaults objectForKey:@"token"]; if (token == nil || [token length] == 0) { if (failure != nil) { failure(@"Invalid Token", -1); } return; } [self showProgressHUD]; NSMutableDictionary *params = [NSMutableDictionary dictionary]; [[self getNetworkingManagerWithToken:token] GET:@"/checktoken" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) { [self hideProgressHUD]; if (success != nil) { success(responseObject); } } failure:^(NSURLSessionTask *operation, NSError *error) { [self hideProgressHUD]; NSString *errorMessage = [self getError:error]; if (failure != nil) { failure(errorMessage, ((NSHTTPURLResponse*)operation.response).statusCode); } }]; }
Maintenant, dans la méthode viewWillAppear de ViewController.m, nous appellerons cette méthode singleton. Remarquez la simplicité de la requête et la petite implémentation du côté de View Controller.
[[NetworkManager sharedManager] tokenCheckWithSuccess:^(id responseObject) { // Allow User Access and load content //[self loadContent]; } failure:^(NSString *failureReason, NSInteger statusCode) { // Logout user if logged in and deny access and show login view //[self showLoginView]; }];
C'est ça! Remarquez comment cet extrait peut être utilisé virtuellement dans n'importe quelle application qui doit vérifier l'authentification au lancement.
De même, nous pouvons gérer une requête POST de connexion : NetworkManager.h :
- (void)authenticateWithEmail:(NSString*)email password:(NSString*)password success:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;
NetworkManager.m :
- (void)authenticateWithEmail:(NSString*)email password:(NSString*)password success:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure { if (email != nil && [email length] > 0 && password != nil && [password length] > 0) { [self showProgressHUD]; NSMutableDictionary *params = [NSMutableDictionary dictionary]; [params setObject:email forKey:@"email"]; [params setObject:password forKey:@"password"]; [[self getNetworkingManagerWithToken:nil] POST:@"/authenticate" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) { [self hideProgressHUD]; if (success != nil) { success(responseObject); } } failure:^(NSURLSessionTask *operation, NSError *error) { [self hideProgressHUD]; NSString *errorMessage = [self getError:error]; if (failure != nil) { failure(errorMessage, ((NSHTTPURLResponse*)operation.response).statusCode); } }]; } else { if (failure != nil) { failure(@"Email and Password Required", -1); } } }
Nous pouvons faire preuve de fantaisie ici et ajouter des alertes avec AlertController + Blocks sur la fenêtre AppDelegate ou simplement renvoyer des objets d'échec au contrôleur de vue. De plus, nous pourrions enregistrer les informations d'identification de l'utilisateur ici ou laisser le contrôleur de vue gérer cela. Habituellement, j'implémente un singleton UserManager séparé qui gère les informations d'identification et les autorisations qui peuvent communiquer directement avec le NetworkManager (préférence personnelle).
Encore une fois, le côté contrôleur de vue est super simple :
- (void)loginUser { NSString *email = @"[email protected]"; NSString *password = @"SomeSillyEasyPassword555"; [[NetworkManager sharedManager] authenticateWithEmail:email password:password success:^(id responseObject) { // Save User Credentials and show content } failure:^(NSString *failureReason, NSInteger statusCode) { // Explain to user why authentication failed }]; }
Oups! Nous avons oublié de versionner l'API et d'envoyer le type d'appareil. De plus, nous avons mis à jour le point de terminaison de "/checktoken" à "/token". Depuis que nous avons centralisé notre réseau, c'est super facile à mettre à jour. Nous n'avons pas besoin de fouiller dans notre code. Puisque nous utiliserons ces paramètres sur toutes les requêtes, nous allons créer un helper.
#define API_VERSION @"1.0" #define DEVICE_TYPE UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? @"tablet" : @"phone" - (NSMutableDictionary*)getBaseParams { NSMutableDictionary *baseParams = [NSMutableDictionary dictionary]; [baseParams setObject:@"version" forKey:API_VERSION]; [baseParams setObject:@"device_type" forKey:DEVICE_TYPE]; return baseParams; }
N'importe quel nombre de paramètres communs peut facilement être ajouté à cela à l'avenir. Ensuite, nous pouvons mettre à jour nos méthodes de vérification et d'authentification des jetons comme suit :
… NSMutableDictionary *params = [self getBaseParams]; [[self getNetworkingManagerWithToken:token] GET:@"/checktoken" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) { … … NSMutableDictionary *params = [self getBaseParams]; [params setObject:email forKey:@"email"]; [params setObject:password forKey:@"password"]; [[self getNetworkingManagerWithToken:nil] POST:@"/authenticate" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) {
Conclusion de notre tutoriel AFNetworking
Nous nous arrêterons ici mais, comme vous pouvez le constater, nous avons centralisé les paramètres et méthodes de mise en réseau communs dans un gestionnaire de singleton, ce qui a grandement simplifié nos implémentations de contrôleur de vue. Les futures mises à jour seront simples et rapides et, plus important encore, elles dissocieront notre réseau de l'expérience utilisateur. La prochaine fois que l'équipe de conception demandera une refonte de l'UI/UX, nous saurons que notre travail est déjà fait du côté du réseau !
Dans cet article, nous nous sommes concentrés sur un singleton réseau mais ces mêmes principes pourraient être appliqués à de nombreuses autres fonctions centralisées telles que :
- Gestion de l'état et des autorisations des utilisateurs
- Routage des actions tactiles vers la navigation de l'application
- Gestion vidéo et audio
- Analytique
- Avis
- Périphériques
- et bien plus encore…
Nous nous sommes également concentrés sur une architecture d'application iOS mais cela pourrait tout aussi bien être étendu à Android et même à JavaScript. En prime, en créant un code hautement défini et axé sur les fonctions, le portage des applications vers de nouvelles plates-formes est une tâche beaucoup plus rapide.
Pour résumer, en consacrant un peu plus de temps à la planification initiale du projet pour établir des méthodes singleton clés, comme l'exemple de mise en réseau ci-dessus, votre futur code peut être plus propre, plus simple et plus maintenable.