Rede centralizada e desacoplada do iOS: Tutorial de AFNetworking com uma classe Singleton

Publicados: 2022-03-11

Quando se trata de padrões de arquitetura iOS, o padrão de design Model-View-Controller (MVC) é ótimo para a longevidade e manutenção da base de código de um aplicativo. Ele permite que as classes sejam facilmente reutilizadas ou substituídas para dar suporte a vários requisitos, desacoplando-as umas das outras. Isso ajuda a maximizar as vantagens da Programação Orientada a Objetos (OOP).

Embora essa arquitetura de aplicativo iOS funcione bem no nível micro (telas/seções individuais de um aplicativo), você pode adicionar funções semelhantes a vários modelos à medida que seu aplicativo cresce. Em casos como redes, mover a lógica comum de suas classes de modelo para classes auxiliares singleton pode ser uma abordagem melhor. Neste tutorial do AFNetworking iOS, ensinarei como configurar um objeto de rede singleton centralizado que, desacoplado de componentes MVC de nível micro, pode ser reutilizado em todo o seu aplicativo de arquitetura desacoplada.

Tutorial AFNetworking: Rede Centralizada e Desacoplada com Singleton

O problema com a rede iOS

A Apple fez um ótimo trabalho ao abstrair muitas das complexidades do gerenciamento de hardware móvel em SDKs iOS fáceis de usar, mas em alguns casos, como rede, Bluetooth, OpenGL e processamento multimídia, as classes podem ser complicadas devido ao seu objetivo de manter os SDKs flexíveis. Felizmente, a rica comunidade de desenvolvedores iOS criou estruturas de alto nível para simplificar os casos de uso mais comuns em um esforço para simplificar o design e a estrutura do aplicativo. Um bom programador, empregando as melhores práticas de arquitetura de aplicativos ios, sabe quais ferramentas usar, por que usá-las e quando é melhor escrever suas próprias ferramentas e classes do zero.

O AFNetworking é um ótimo exemplo de rede e uma das estruturas de código aberto mais usadas, o que simplifica as tarefas diárias de um desenvolvedor. Ele simplifica a rede de API RESTful e cria padrões modulares de solicitação/resposta com blocos de conclusão de sucesso, progresso e falha. Isso elimina a necessidade de métodos delegados implementados pelo desenvolvedor e configurações personalizadas de solicitação/conexão e pode ser incluído em qualquer classe muito rapidamente.

O problema com o AFNetworking

O AFNetworking é ótimo, mas sua modularidade também pode levar ao seu uso de forma fragmentada. Implementações ineficientes comuns podem incluir:

  • Várias solicitações de rede usando métodos e propriedades semelhantes em um único controlador de exibição

  • Solicitações quase idênticas em vários controladores de exibição que levam a variáveis ​​comuns distribuídas que podem ficar fora de sincronia

  • Solicitações de rede em uma classe para dados não relacionados a essa classe

Para aplicativos com número limitado de visualizações, poucas chamadas de API a serem implementadas e aquelas que provavelmente não serão alteradas com frequência, isso pode não ser uma grande preocupação. No entanto, é mais provável que você esteja pensando grande e tenha muitos anos de atualizações planejadas. Se o seu caso for o último, você provavelmente precisará lidar com:

  • Versionamento de API para dar suporte a várias gerações de um aplicativo

  • Adição de novos parâmetros ou alterações nos parâmetros existentes ao longo do tempo para expandir a capacidade

  • Implementação de APIs totalmente novas

Se o seu código de rede estiver espalhado por toda a sua base de código, isso agora é um pesadelo em potencial. Felizmente, você tem pelo menos alguns de seus parâmetros definidos estaticamente em um cabeçalho comum, mas mesmo assim você pode tocar em uma dúzia de classes até mesmo para as alterações mais pequenas.

Como abordamos as limitações do AFNetworking?

Crie um singleton de rede para centralizar o tratamento de solicitações, respostas e seus parâmetros.

Um objeto singleton fornece um ponto global de acesso aos recursos de sua classe. Singletons são usados ​​em situações onde este único ponto de controle é desejável, como em classes que oferecem algum serviço ou recurso geral. Você obtém a instância global de uma classe singleton por meio de um método de fábrica. - Maçã

Portanto, um singleton é uma classe da qual você teria apenas uma instância em um aplicativo que existe durante a vida útil do aplicativo. Além disso, como sabemos que há apenas uma instância, ela é facilmente acessível por qualquer outra classe que precise acessar seus métodos ou propriedades.

Veja por que devemos usar um singleton para rede:

  • Ele é inicializado estaticamente, portanto, uma vez criado, terá os mesmos métodos e propriedades disponíveis para qualquer classe que tentar acessá-lo. Não há chance de problemas estranhos de sincronização ou solicitação de dados da instância errada de uma classe.

  • Você pode limitar suas chamadas de API para ficarem abaixo de um limite de taxa (por exemplo, quando você precisa manter suas solicitações de API abaixo de cinco por segundo).

  • Propriedades estáticas, como nome do host, números de porta, terminais, versão da API, tipo de dispositivo, IDs persistentes, tamanho da tela, etc. podem ser colocadas no mesmo local para que uma alteração afete todas as solicitações de rede.

  • Propriedades comuns podem ser reutilizadas entre muitas solicitações de rede.

  • O objeto singleton não ocupa memória até que seja instanciado. Isso pode ser útil para singletons com casos de uso muito específicos que alguns usuários podem nunca precisar, como lidar com transmissão de vídeo para um Chromecast se eles não tiverem o dispositivo.

  • As solicitações de rede podem ser completamente desacopladas das visualizações e dos controladores para que possam continuar mesmo depois que as visualizações e os controladores forem destruídos.

  • O log de rede pode ser centralizado e simplificado.

  • Eventos comuns de falha, como alertas, podem ser reutilizados para todas as solicitações.

  • A estrutura principal de tal singleton pode ser reutilizada em vários projetos com alterações simples de propriedades estáticas de nível superior.

Algumas razões para não usar singletons:

  • Eles podem ser usados ​​em excesso para fornecer várias responsabilidades em uma única classe. Por exemplo, métodos de processamento de vídeo podem ser misturados com métodos de rede ou métodos de estado do usuário. Isso provavelmente seria uma prática de design ruim e levaria a um código difícil de entender. Em vez disso, vários singletons com responsabilidades específicas devem ser criados.

  • Singletons não podem ser subclassificados.

  • Singletons podem ocultar dependências e, assim, tornar-se menos modulares. Por exemplo, se um singleton for removido e uma classe não tiver uma importação importada pelo singleton, isso poderá levar a problemas futuros (especialmente se houver dependências de bibliotecas externas).

  • Uma classe pode modificar propriedades compartilhadas em singletons durante operações longas que são inesperadas em outra classe. Sem uma reflexão adequada sobre isso, os resultados podem variar.

  • Vazamentos de memória em um singleton podem se tornar um problema significativo, pois o próprio singleton nunca é desalocado.

No entanto, usando as práticas recomendadas da arquitetura de aplicativos iOS, esses pontos negativos podem ser atenuados. Algumas práticas recomendadas incluem:

  • Cada singleton deve lidar com uma única responsabilidade.

  • Não use singletons para armazenar dados que serão alterados rapidamente por várias classes ou threads se você precisar de alta precisão.

  • Crie singletons para ativar/desativar recursos com base nas dependências disponíveis.

  • Não armazene grandes quantidades de dados em propriedades singleton, pois eles persistirão durante a vida útil do seu aplicativo (a menos que sejam gerenciados manualmente).

Um exemplo simples de Singleton com AFNetworking

Primeiro, como pré-requisito, adicione o AFNetworking ao seu projeto. A abordagem mais simples é via Cocoapods e as instruções são encontradas em sua página do GitHub.

Enquanto você está nisso, sugiro adicionar UIAlertController+Blocks e MBProgressHUD (mais uma vez facilmente adicionados com CocoaPods). Eles são claramente opcionais, mas isso simplificará bastante o progresso e os alertas, caso você deseje implementá-los no singleton na janela do AppDelegate.

Depois que AFNetworking for adicionado, comece criando uma nova Cocoa Touch Class chamada NetworkManager como uma subclasse de NSObject . Adicione um método de classe para acessar o gerenciador. Seu arquivo NetworkManager.h deve se parecer com o código abaixo:

 #import <Foundation/Foundation.h> #import “AFNetworking.h” @interface NetworkManager : NSObject + (id)sharedManager; @end

Em seguida, implemente os métodos básicos de inicialização para o singleton e importe o cabeçalho AFNetworking. Sua implementação de classe deve ter a seguinte aparência (OBSERVAÇÃO: isso pressupõe que você esteja usando a contagem automática de referências):

 #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

Excelente! Agora estamos cozinhando e prontos para adicionar propriedades e métodos. Como um teste rápido para entender como acessar um singleton, vamos adicionar o seguinte a NetworkManager.h :

 @property NSString *appID; - (void)test;

E o seguinte para 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); }

Em seguida, em nosso arquivo principal ViewController.m (ou o que você tiver), importe NetworkManager.h em viewDidLoad adicione:

 [[NetworkManager sharedManager] test];

Inicie o aplicativo e você deverá ver o seguinte na saída:

Testing our the networking singleton for appID: 1, HOST: http://www.apitesting.dev/, and PORT: 80

Ok, então você provavelmente não vai misturar #define , static const e @property de uma só vez assim, mas simplesmente mostrando suas opções para maior clareza. “static const” é uma declaração melhor para segurança de tipo, mas #define pode ser útil na construção de strings, pois permite o uso de macros. Para o que vale a pena, estou usando #define para brevidade neste cenário. A menos que você esteja usando ponteiros, não há muita diferença na prática entre essas abordagens de declaração.

Agora que você entende #defines , constantes, propriedades e métodos, podemos removê-los e passar para exemplos mais relevantes.

Um exemplo de rede

Imagine um aplicativo onde o usuário deve estar logado para acessar qualquer coisa. Ao iniciar o aplicativo, verificaremos se salvamos um token de autenticação e, em caso afirmativo, realizaremos uma solicitação GET para nossa API para ver se o token expirou ou não.

Em AppDelegate.m , vamos registrar um padrão para nosso token:

 + (void)initialize { NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:@"", @"token", nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; }

Adicionaremos uma verificação de token ao NetworkManager e obteremos feedback sobre a verificação por meio de blocos de conclusão. Você pode projetar esses blocos de conclusão como quiser. Neste exemplo, estou usando o sucesso com os dados do objeto de resposta e a falha com a string de resposta de erro e um código de status. Observação: a falha pode ser opcionalmente deixada de fora se não for importante para o lado do recebimento, como incrementar um valor na análise.

NetworkManager.h

Acima @interface :

 typedef void (^NetworkManagerSuccess)(id responseObject); typedef void (^NetworkManagerFailure)(NSString *failureReason, NSInteger statusCode);

Na @interface:

@property (não atômico, forte) AFHTTPSessionManager *networkingManager;

 - (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;

NetworkManager.m:

Defina nosso 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]

Adicionaremos alguns métodos auxiliares para simplificar as solicitações autenticadas, bem como erros de análise (este exemplo usa um token da 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"; }

Se você adicionou MBProgressHUD, ele pode ser usado aqui:

 #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; } }

E nossa solicitação de verificação 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); } }]; }

Agora, no método ViewController.m viewWillAppear, chamaremos esse método singleton. Observe a simplicidade da solicitação e a pequena implementação no lado do 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]; }];

É isso! Observe como esse trecho pode ser usado virtualmente em qualquer aplicativo que precise verificar a autenticação na inicialização.

Da mesma forma, podemos lidar com uma solicitação POST para login: 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 ser sofisticados aqui e adicionar alertas com AlertController+Blocks na janela AppDelegate ou simplesmente enviar objetos de falha de volta ao controlador de exibição. Além disso, podemos salvar as credenciais do usuário aqui ou, em vez disso, deixar o controlador de exibição lidar com isso. Normalmente, implemento um singleton UserManager separado que lida com credenciais e permissões que podem se comunicar diretamente com o NetworkManager (preferência pessoal).

Mais uma vez, o lado do controlador de visualização é super simples:

 - (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 }]; }

Ops! Esquecemos de versão da API e enviar o tipo de dispositivo. Além disso, atualizamos o endpoint de “/checktoken” para “/token”. Como centralizamos nossa rede, isso é super fácil de atualizar. Não precisamos vasculhar nosso código. Como usaremos esses parâmetros em todas as solicitações, criaremos um auxiliar.

 #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; }

Qualquer número de parâmetros comuns pode ser facilmente adicionado a isso no futuro. Em seguida, podemos atualizar nossa verificação de token e autenticar métodos da seguinte forma:

 … 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) {

Finalizando nosso tutorial de AFNetworking

Vamos parar por aqui, mas, como você pode ver, centralizamos parâmetros e métodos comuns de rede em um gerenciador singleton, o que simplificou bastante nossas implementações de controlador de exibição. As atualizações futuras serão simples e rápidas e, o mais importante, dissociarão nossa rede da experiência do usuário. Da próxima vez que a equipe de design pedir uma revisão de UI/UX, saberemos que nosso trabalho já está feito no lado da rede!

Neste artigo, focamos em um singleton de rede, mas esses mesmos princípios podem ser aplicados a muitas outras funções centralizadas, como:

  • Manipulando o estado e as permissões do usuário
  • Roteamento de ações de toque para a navegação do aplicativo
  • Gerenciamento de vídeo e áudio
  • Análise
  • Notificações
  • Periféricos
  • e muito mais…

Também nos concentramos em uma arquitetura de aplicativo iOS, mas isso pode ser facilmente estendido para Android e até JavaScript. Como bônus, ao criar um código altamente definido e orientado a funções, torna a portar aplicativos para novas plataformas uma tarefa muito mais rápida.

Para resumir, gastando um pouco de tempo extra no planejamento inicial do projeto para estabelecer os principais métodos singleton, como o exemplo de rede acima, seu código futuro pode ser mais limpo, mais simples e mais fácil de manter.