Redes centralizadas y desacopladas de iOS: tutorial de AFNetworking con una clase Singleton
Publicado: 2022-03-11Cuando se trata de patrones de arquitectura de iOS, el patrón de diseño Modelo-Vista-Controlador (MVC) es excelente para la longevidad y la capacidad de mantenimiento de la base de código de una aplicación. Permite que las clases se reutilicen o reemplacen fácilmente para admitir diversos requisitos al desacoplarlas entre sí. Esto ayuda a maximizar las ventajas de la Programación Orientada a Objetos (POO).
Si bien esta arquitectura de aplicaciones de iOS funciona bien en el nivel micro (pantallas/secciones individuales de una aplicación), es posible que agregue funciones similares a varios modelos a medida que crece su aplicación. En casos como las redes, mover la lógica común fuera de las clases modelo y hacia las clases auxiliares únicas puede ser un mejor enfoque. En este tutorial de iOS de AFNetworking, le enseñaré cómo configurar un objeto de red singleton centralizado que, desacoplado de componentes MVC de nivel micro, se puede reutilizar en toda su aplicación de arquitectura desacoplada.
El problema con las redes de iOS
Apple ha hecho un gran trabajo al abstraer muchas de las complejidades de administrar el hardware móvil en SDK de iOS fáciles de usar, pero en algunos casos, como redes, Bluetooth, OpenGL y procesamiento multimedia, las clases pueden ser engorrosas debido a su objetivo de mantener los SDK son flexibles. Afortunadamente, la rica comunidad de desarrolladores de iOS ha creado marcos de alto nivel para simplificar los casos de uso más comunes en un esfuerzo por simplificar el diseño y la estructura de la aplicación. Un buen programador, que emplea las mejores prácticas de arquitectura de aplicaciones de ios, sabe qué herramientas usar, por qué usarlas y cuándo es mejor escribir sus propias herramientas y clases desde cero.
AFNetworking es un excelente ejemplo de redes y uno de los marcos de código abierto más utilizados, que simplifica las tareas diarias de un desarrollador. Simplifica las redes de API RESTful y crea patrones modulares de solicitud/respuesta con bloques de finalización de éxito, progreso y falla. Esto elimina la necesidad de métodos de delegado implementados por el desarrollador y configuraciones personalizadas de solicitud/conexión y se puede incluir en cualquier clase muy rápidamente.
El problema con AFNetworking
AFNetworking es excelente, pero su modularidad también puede conducir a su uso de manera fragmentada. Las implementaciones ineficientes comunes pueden incluir:
Múltiples solicitudes de red usando métodos y propiedades similares en un solo controlador de vista
Solicitudes casi idénticas en múltiples controladores de vista que conducen a variables comunes distribuidas que pueden perder la sincronización
Solicitudes de red en una clase para datos no relacionados con esa clase
Para aplicaciones con un número limitado de vistas, pocas llamadas API para implementar y que probablemente no cambien con frecuencia, esto puede no ser una gran preocupación. Sin embargo, lo más probable es que esté pensando en grande y tenga planeados muchos años de actualizaciones. Si su caso es el último, es probable que termine necesitando manejar:
Control de versiones de API para admitir múltiples generaciones de una aplicación
Adición de nuevos parámetros o cambios en los parámetros existentes a lo largo del tiempo para ampliar la capacidad
Implementación de APIs totalmente nuevas
Si su código de red está disperso por todo su código base, esto ahora es una pesadilla potencial. Con suerte, tiene al menos algunos de sus parámetros definidos estáticamente en un encabezado común, pero incluso entonces puede tocar una docena de clases incluso para los cambios más pequeños.
¿Cómo abordamos las limitaciones de AFNetworking?
Cree un singleton de red para centralizar el manejo de solicitudes, respuestas y sus parámetros.
Un objeto singleton proporciona un punto de acceso global a los recursos de su clase. Los singletons se utilizan en situaciones en las que este único punto de control es deseable, como con clases que ofrecen algún servicio o recurso general. Obtiene la instancia global de una clase singleton a través de un método de fábrica. - Manzana
Entonces, un singleton es una clase de la que solo tendría una instancia en una aplicación que existe durante la vida de la aplicación. Además, debido a que sabemos que solo hay una instancia, cualquier otra clase que necesite acceder a sus métodos o propiedades puede acceder fácilmente a ella.
He aquí por qué deberíamos usar un singleton para la creación de redes:
Se inicializa estáticamente, por lo que, una vez creado, tendrá los mismos métodos y propiedades disponibles para cualquier clase que intente acceder a él. No hay posibilidad de problemas de sincronización extraños o de solicitar datos de la instancia incorrecta de una clase.
Puede limitar sus llamadas API para permanecer por debajo de un límite de velocidad (por ejemplo, cuando tiene que mantener sus solicitudes API por debajo de cinco por segundo).
Las propiedades estáticas, como el nombre de host, los números de puerto, los puntos finales, la versión de la API, el tipo de dispositivo, las ID persistentes, el tamaño de la pantalla, etc., se pueden ubicar en el mismo lugar para que un cambio afecte a todas las solicitudes de la red.
Las propiedades comunes se pueden reutilizar entre muchas solicitudes de red.
El objeto singleton no ocupa memoria hasta que se crea una instancia. Esto puede ser útil para singletons con casos de uso muy específicos que algunos usuarios nunca necesitarán, como manejar la transmisión de video a un Chromecast si no tienen el dispositivo.
Las solicitudes de red se pueden desacoplar por completo de las vistas y los controladores para que puedan continuar incluso después de que se destruyan las vistas y los controladores.
El registro de red se puede centralizar y simplificar.
Los eventos comunes de falla, como las alertas, se pueden reutilizar para todas las solicitudes.
La estructura principal de dicho singleton podría reutilizarse en múltiples proyectos con simples cambios de propiedad estática de nivel superior.
Algunas razones para no usar singletons:
Se pueden usar en exceso para proporcionar múltiples responsabilidades en una sola clase. Por ejemplo, los métodos de procesamiento de video podrían combinarse con métodos de redes o métodos de estado de usuario. Esto probablemente sería una mala práctica de diseño y conduciría a un código difícil de entender. En su lugar, se deben crear múltiples singletons con responsabilidades específicas.
Singletons no se pueden subclasificar.
Singletons puede ocultar dependencias y, por lo tanto, volverse menos modular. Por ejemplo, si se elimina un singleton y a una clase le faltaba una importación que importó el singleton, puede generar problemas en el futuro (especialmente si hay dependencias de bibliotecas externas).
Una clase puede modificar propiedades compartidas en singletons durante operaciones largas que no son esperadas en otra clase. Sin una reflexión adecuada sobre esto, los resultados pueden variar.
Las fugas de memoria en un singleton pueden convertirse en un problema importante, ya que el singleton nunca se desasigna.
Sin embargo, al utilizar las mejores prácticas de la arquitectura de aplicaciones de iOS, estos aspectos negativos pueden aliviarse. Algunas mejores prácticas incluyen:
Cada singleton debe manejar una sola responsabilidad.
No use singletons para almacenar datos que varias clases o subprocesos cambiarán rápidamente si necesita una alta precisión.
Cree singletons para habilitar/deshabilitar funciones en función de las dependencias disponibles.
No almacene grandes cantidades de datos en propiedades singleton, ya que persistirán durante la vida útil de su aplicación (a menos que se administren manualmente).
Un ejemplo simple de Singleton con AFNetworking
Primero, como requisito previo, agregue AFNetworking a su proyecto. El enfoque más simple es a través de Cocoapods y las instrucciones se encuentran en su página de GitHub.
Mientras lo hace, le sugiero que agregue UIAlertController+Blocks
y MBProgressHUD
(nuevamente se agrega fácilmente con CocoaPods). Estos son claramente opcionales, pero esto simplificará en gran medida el progreso y las alertas si desea implementarlos en el singleton en la ventana de AppDelegate.
Una vez que se agrega AFNetworking
, comience creando una nueva clase Cocoa Touch llamada NetworkManager
como una subclase de NSObject
. Agregue un método de clase para acceder al administrador. Su archivo NetworkManager.h
debería verse como el siguiente código:
#import <Foundation/Foundation.h> #import “AFNetworking.h” @interface NetworkManager : NSObject + (id)sharedManager; @end
A continuación, implemente los métodos de inicialización básicos para el singleton e importe el encabezado AFNetworking. La implementación de su clase debería tener el siguiente aspecto (NOTA: esto supone que está utilizando el conteo automático de referencias):
#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
¡Genial! Ahora estamos cocinando y listos para agregar propiedades y métodos. Como una prueba rápida para comprender cómo acceder a un singleton, agreguemos lo siguiente a NetworkManager.h
:
@property NSString *appID; - (void)test;
Y lo siguiente a 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); }
Luego, en nuestro archivo principal ViewController.m
(o lo que sea que tenga), importe NetworkManager.h
y luego en viewDidLoad
agregue:
[[NetworkManager sharedManager] test];
Inicie la aplicación y debería ver lo siguiente en el resultado:
Testing our the networking singleton for appID: 1, HOST: http://www.apitesting.dev/, and PORT: 80
De acuerdo, es probable que no mezcle #define
, static const y @property
todos a la vez de esta manera, sino que simplemente muestre sus opciones para mayor claridad. "static const" es una mejor declaración para la seguridad de tipos, pero #define
puede ser útil en la construcción de cadenas, ya que permite el uso de macros. Por lo que vale, estoy usando #define
por brevedad en este escenario. A menos que esté utilizando punteros, no hay mucha diferencia en la práctica entre estos enfoques de declaración.
Ahora que comprende #defines
, constantes, propiedades y métodos, podemos eliminarlos y pasar a ejemplos más relevantes.

Un ejemplo de red
Imagine una aplicación en la que el usuario debe iniciar sesión para acceder a cualquier cosa. Al iniciar la aplicación, comprobaremos si hemos guardado un token de autenticación y, de ser así, realizaremos una solicitud GET a nuestra API para ver si el token ha caducado o no.
En AppDelegate.m
, registremos un valor predeterminado para nuestro token:
+ (void)initialize { NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:@"", @"token", nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; }
Agregaremos una verificación de token a NetworkManager y obtendremos comentarios sobre la verificación a través de bloques de finalización. Puede diseñar estos bloques de finalización como desee. En este ejemplo, uso el éxito con los datos del objeto de respuesta y el error con la cadena de respuesta de error y un código de estado. Nota: La falla puede omitirse opcionalmente si no le importa al lado receptor, como incrementar un valor en el análisis.
NetworkManager.h
Por encima @interface
:
typedef void (^NetworkManagerSuccess)(id responseObject); typedef void (^NetworkManagerFailure)(NSString *failureReason, NSInteger statusCode);
En @interfaz:
@property (no atómico, fuerte) AFHTTPSessionManager *networkingManager;
- (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;
NetworkManager.m:
Defina nuestra 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]
Agregaremos algunos métodos de ayuda para simplificar las solicitudes autenticadas y los errores de análisis (este ejemplo usa un token 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 agregó MBProgressHUD, se puede usar aquí:
#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; } }
Y nuestra solicitud de verificación de token:
- (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); } }]; }
Ahora, en el método ViewController.m viewWillAppear, llamaremos a este método singleton. Observe la simplicidad de la solicitud y la pequeña implementación en el lado del controlador de vista.
[[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]; }];
¡Eso es todo! Observe cómo este fragmento podría usarse virtualmente en cualquier aplicación que necesite verificar la autenticación al iniciarse.
De manera similar, podemos manejar una solicitud POST para iniciar sesión: 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); } } }
Podemos ponernos sofisticados aquí y agregar alertas con AlertController+Blocks en la ventana de AppDelegate o simplemente enviar objetos fallidos al controlador de vista. Además, podríamos guardar las credenciales de usuario aquí o dejar que el controlador de vista maneje eso. Por lo general, implemento un singleton de UserManager separado que maneja las credenciales y los permisos que pueden comunicarse directamente con NetworkManager (preferencia personal).
Una vez más, el lado del controlador de vista es súper 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 }]; }
¡UPS! Nos olvidamos de versionar la API y enviar el tipo de dispositivo. Además, hemos actualizado el punto final de "/checktoken" a "/token". Dado que centralizamos nuestra red, esto es muy fácil de actualizar. No necesitamos cavar a través de nuestro código. Dado que usaremos estos parámetros en todas las solicitudes, crearemos un asistente.
#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; }
Cualquier número de parámetros comunes se puede agregar fácilmente a esto en el futuro. Luego, podemos actualizar nuestra verificación de tokens y métodos de autenticación de esta manera:
… 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) {
Terminando nuestro tutorial de AFNetworking
Nos detendremos aquí pero, como puede ver, hemos centralizado los parámetros y métodos de red comunes en un administrador único, lo que ha simplificado enormemente nuestras implementaciones de controladores de vista. Las actualizaciones futuras serán simples y rápidas y, lo más importante, desvincularán nuestra red de la experiencia del usuario. La próxima vez que el equipo de diseño solicite una revisión de UI/UX, ¡sabremos que nuestro trabajo ya está hecho en el lado de la red!
En este artículo nos enfocamos en un singleton de red, pero estos mismos principios podrían aplicarse a muchas otras funciones centralizadas, tales como:
- Manejo del estado y los permisos del usuario
- Enrutamiento de acciones táctiles a la navegación de la aplicación
- Gestión de vídeo y audio
- Analítica
- Notificaciones
- Periféricos
- y mucho, mucho más…
También nos enfocamos en una arquitectura de aplicaciones para iOS, pero esto podría extenderse fácilmente a Android e incluso a JavaScript. Como beneficio adicional, al crear un código altamente definido y orientado a funciones, hace que la migración de aplicaciones a nuevas plataformas sea una tarea mucho más rápida.
En resumen, al dedicar un poco más de tiempo en la planificación inicial del proyecto para establecer métodos clave de singleton, como el ejemplo de red anterior, su código futuro puede ser más limpio, más simple y más fácil de mantener.