Reti centralizzate e disaccoppiate iOS: tutorial AFNetworking con una classe Singleton

Pubblicato: 2022-03-11

Quando si tratta di modelli di architettura iOS, il modello di progettazione Model-View-Controller (MVC) è ottimo per la longevità e la manutenibilità della base di codice di un'applicazione. Consente di riutilizzare o sostituire facilmente le classi per supportare vari requisiti disaccoppiandole l'una dall'altra. Questo aiuta a massimizzare i vantaggi della programmazione orientata agli oggetti (OOP).

Sebbene questa architettura dell'applicazione iOS funzioni bene a livello micro (singole schermate/sezioni di un'app), potresti ritrovarti ad aggiungere funzioni simili a più modelli man mano che la tua app cresce. In casi come il networking, lo spostamento della logica comune dalle classi del modello alle classi helper singleton può essere un approccio migliore. In questo tutorial per iOS AFNetworking, ti insegnerò come configurare un oggetto di rete singleton centralizzato che, disaccoppiato dai componenti MVC di livello micro, può essere riutilizzato in tutta la tua applicazione di architettura disaccoppiata.

Tutorial AFNetworking: networking centralizzato e disaccoppiato con Singleton

Il problema con la rete iOS

Apple ha fatto un ottimo lavoro nell'atrarre molte delle complessità della gestione dell'hardware mobile in SDK iOS facili da usare, ma in alcuni casi, come il networking, Bluetooth, OpenGL e l'elaborazione multimediale, le classi possono essere ingombranti a causa del loro obiettivo di mantenere gli SDK flessibili. Per fortuna, la ricca comunità di sviluppatori iOS ha creato framework di alto livello per semplificare i casi d'uso più comuni nel tentativo di semplificare la progettazione e la struttura dell'applicazione. Un buon programmatore, che utilizza le migliori pratiche dell'architettura di app ios, sa quali strumenti utilizzare, perché usarli e quando è meglio scrivere i propri strumenti e classi da zero.

AFNetworking è un ottimo esempio di rete e uno dei framework open source più comunemente usati, che semplifica le attività quotidiane di uno sviluppatore. Semplifica la rete dell'API RESTful e crea modelli di richiesta/risposta modulari con blocchi di completamento di successo, avanzamento e errore. Ciò elimina la necessità di metodi delegati implementati dagli sviluppatori e di impostazioni di richiesta/connessione personalizzate e può essere incluso in qualsiasi classe molto rapidamente.

Il problema con AFNetworking

AFNetworking è ottimo, ma la sua modularità può anche portare al suo utilizzo in modo frammentato. Le implementazioni inefficienti comuni possono includere:

  • Richieste di rete multiple che utilizzano metodi e proprietà simili in un unico controller di visualizzazione

  • Richieste quasi identiche in più controller di visualizzazione che portano a variabili comuni distribuite che possono non essere sincronizzate

  • Richieste di rete in una classe per dati non correlati a quella classe

Per le applicazioni con un numero limitato di visualizzazioni, poche chiamate API da implementare e quelle che probabilmente non cambieranno spesso, questo potrebbe non essere di grande preoccupazione. Tuttavia, è più probabile che tu stia pensando in grande e hai pianificato molti anni di aggiornamenti. Se il tuo caso è quest'ultimo, probabilmente finirai per dover gestire:

  • Versione API per supportare più generazioni di un'applicazione

  • Aggiunta di nuovi parametri o modifiche a parametri esistenti nel tempo per espandere la capacità

  • Implementazione di API totalmente nuove

Se il tuo codice di rete è sparso su tutta la tua base di codice, questo è ora un potenziale incubo. Si spera che almeno alcuni dei tuoi parametri siano definiti staticamente in un'intestazione comune, ma anche in questo caso potresti toccare una dozzina di classi anche per le modifiche più piccole.

Come affrontiamo le limitazioni di AFNetworking?

Crea un singleton di rete per centralizzare la gestione di richieste, risposte e relativi parametri.

Un oggetto singleton fornisce un punto di accesso globale alle risorse della sua classe. I singleton vengono utilizzati in situazioni in cui questo singolo punto di controllo è desiderabile, ad esempio con classi che offrono servizi o risorse generali. Ottieni l'istanza globale da una classe singleton tramite un metodo factory. - Mela

Quindi, un singleton è una classe di cui avresti sempre e solo un'istanza in un'applicazione che esiste per la vita dell'applicazione. Inoltre, poiché sappiamo che esiste solo un'istanza, è facilmente accessibile da qualsiasi altra classe che deve accedere ai suoi metodi o proprietà.

Ecco perché dovremmo usare un singleton per il networking:

  • È inizializzato staticamente quindi, una volta creato, avrà gli stessi metodi e proprietà disponibili per qualsiasi classe che tenti di accedervi. Non è possibile che si verifichino problemi di sincronizzazione dispari o che richiedano dati dall'istanza errata di una classe.

  • Puoi limitare le tue chiamate API per rimanere al di sotto di un limite di velocità (ad esempio, quando devi mantenere le tue richieste API al di sotto di cinque al secondo).

  • Proprietà statiche come nome host, numeri di porta, endpoint, versione API, tipo di dispositivo, ID persistenti, dimensioni dello schermo, ecc. possono essere posizionate insieme in modo che una modifica influisca su tutte le richieste di rete.

  • Le proprietà comuni possono essere riutilizzate tra molte richieste di rete.

  • L'oggetto singleton non occupa memoria finché non viene istanziato. Questo può essere utile per singleton con casi d'uso molto specifici di cui alcuni utenti potrebbero non aver mai bisogno, come la gestione della trasmissione di video su un Chromecast se non hanno il dispositivo.

  • Le richieste di rete possono essere completamente disaccoppiate da visualizzazioni e controller in modo che possano continuare anche dopo la distruzione di visualizzazioni e controller.

  • La registrazione di rete può essere centralizzata e semplificata.

  • Gli eventi comuni di errore come gli avvisi possono essere riutilizzati per tutte le richieste.

  • La struttura principale di un tale singleton potrebbe essere riutilizzata su più progetti con semplici modifiche alle proprietà statiche di primo livello.

Alcuni motivi per non utilizzare i singleton:

  • Possono essere abusati per fornire più responsabilità in una singola classe. Ad esempio, i metodi di elaborazione video possono essere combinati con metodi di rete o metodi di stato utente. Questa sarebbe probabilmente una pratica di progettazione scadente e porterebbe a codice difficile da capire. Invece, dovrebbero essere creati più singleton con responsabilità specifiche.

  • I singleton non possono essere sottoclassi.

  • I singleton possono nascondere le dipendenze e quindi diventare meno modulari. Ad esempio, se un singleton viene rimosso e a una classe mancava un'importazione importata da singleton, potrebbero verificarsi problemi futuri (soprattutto se sono presenti dipendenze di librerie esterne).

  • Una classe può modificare le proprietà condivise nei singleton durante lunghe operazioni che sono impreviste in un'altra classe. Senza un'adeguata riflessione su questo, i risultati possono variare.

  • Le perdite di memoria in un singleton possono diventare un problema significativo poiché il singleton stesso non viene mai deallocato.

Tuttavia, utilizzando le migliori pratiche dell'architettura dell'app iOS, questi aspetti negativi possono essere alleviati. Alcune buone pratiche includono:

  • Ogni singleton dovrebbe gestire una singola responsabilità.

  • Non utilizzare singleton per archiviare dati che verranno modificati rapidamente da più classi o thread se è necessaria un'elevata precisione.

  • Crea singleton per abilitare/disabilitare le funzionalità in base alle dipendenze disponibili.

  • Non archiviare grandi quantità di dati in proprietà singleton poiché rimarranno per tutta la vita dell'applicazione (a meno che non siano gestite manualmente).

Un semplice esempio singleton con AFNetworking

Innanzitutto, come prerequisito, aggiungi AFNetworking al tuo progetto. L'approccio più semplice è tramite Cocoapods e le istruzioni si trovano nella sua pagina GitHub.

Già che ci sei, ti suggerisco di aggiungere UIAlertController+Blocks e MBProgressHUD (di nuovo facilmente aggiunti con CocoaPods). Questi sono chiaramente facoltativi, ma questo semplificherà notevolmente i progressi e gli avvisi se desideri implementarli nel singleton nella finestra di AppDelegate.

Una volta aggiunto AFNetworking , inizia creando una nuova classe Cocoa Touch denominata NetworkManager come sottoclasse di NSObject . Aggiungi un metodo di classe per accedere al gestore. Il tuo file NetworkManager.h dovrebbe assomigliare al codice seguente:

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

Quindi, implementa i metodi di inizializzazione di base per il singleton e importa l'intestazione AFNetworking. L'implementazione della tua classe dovrebbe essere simile alla seguente (NOTA: questo presuppone che tu stia utilizzando il conteggio automatico dei riferimenti):

 #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

Grande! Ora stiamo cucinando e siamo pronti per aggiungere proprietà e metodi. Come test rapido per capire come accedere a un singleton, aggiungiamo quanto segue a NetworkManager.h :

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

E quanto segue 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); }

Quindi nel nostro file principale ViewController.m (o qualunque cosa tu abbia), importa NetworkManager.h e poi in viewDidLoad aggiungi:

 [[NetworkManager sharedManager] test];

Avvia l'app e dovresti vedere quanto segue nell'output:

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

Ok, quindi probabilmente non mescolerai #define , static const e @property tutto in una volta in questo modo, ma mostrerai semplicemente per chiarezza le tue opzioni. "static const" è una dichiarazione migliore per la sicurezza dei tipi, ma #define può essere utile nella creazione di stringhe poiché consente l'uso di macro. Per quello che vale, sto usando #define per brevità in questo scenario. A meno che non si utilizzino i puntatori, non c'è molta differenza nella pratica tra questi approcci di dichiarazione.

Ora che hai compreso #defines , costanti, proprietà e metodi, possiamo rimuoverli e passare a esempi più rilevanti.

Un esempio di rete

Immagina un'applicazione in cui l'utente deve essere loggato per accedere a qualsiasi cosa. All'avvio dell'app, verificheremo se abbiamo salvato un token di autenticazione e, in tal caso, eseguiremo una richiesta GET alla nostra API per vedere se il token è scaduto o meno.

In AppDelegate.m , registriamo un valore predefinito per il nostro token:

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

Aggiungeremo un controllo del token a NetworkManager e riceveremo un feedback sul controllo tramite i blocchi di completamento. Puoi progettare questi blocchi di completamento come preferisci. In questo esempio, sto usando il successo con i dati dell'oggetto di risposta e l'errore con la stringa di risposta dell'errore e un codice di stato. Nota: l'errore può essere facoltativamente escluso se non è importante per il lato ricevente, ad esempio l'incremento di un valore nell'analisi.

NetworkManager.h

Sopra @interface :

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

In @interfaccia:

@property (non anatomico, forte) AFHTTPSessionManager *networkingManager;

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

NetworkManager.m:

Definisci il nostro 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]

Aggiungeremo alcuni metodi di supporto per semplificare le richieste autenticate e per gli errori di analisi (questo esempio utilizza 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"; }

Se hai aggiunto MBProgressHUD, può essere utilizzato qui:

 #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 la nostra richiesta di verifica dei 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); } }]; }

Ora, nel metodo ViewController.m viewWillAppear, chiameremo questo metodo singleton. Notare la semplicità della richiesta e la piccola implementazione sul lato 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]; }];

Questo è tutto! Nota come questo frammento di codice potrebbe essere utilizzato virtualmente in qualsiasi applicazione che deve controllare l'autenticazione all'avvio.

Allo stesso modo, possiamo gestire una richiesta POST per l'accesso: 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); } } }

Possiamo essere fantasiosi qui e aggiungere avvisi con AlertController+Blocks nella finestra AppDelegate o semplicemente inviare gli oggetti di errore al controller di visualizzazione. Inoltre, potremmo salvare qui le credenziali dell'utente o lasciare che sia il controller di visualizzazione a gestirlo. Di solito, implemento un singleton UserManager separato che gestisce credenziali e autorizzazioni in grado di comunicare direttamente con NetworkManager (preferenza personale).

Ancora una volta, il lato del controller di visualizzazione è semplicissimo:

 - (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! Abbiamo dimenticato di eseguire la versione dell'API e di inviare il tipo di dispositivo. Inoltre, abbiamo aggiornato l'endpoint da "/checktoken" a "/token". Dal momento che abbiamo centralizzato la nostra rete, è semplicissimo da aggiornare. Non abbiamo bisogno di scavare nel nostro codice. Poiché utilizzeremo questi parametri su tutte le richieste, creeremo 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; }

Qualsiasi numero di parametri comuni può essere facilmente aggiunto a questo in futuro. Quindi possiamo aggiornare il nostro controllo dei token e autenticare i metodi in questo modo:

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

Conclusione del nostro tutorial di AFNetworking

Ci fermeremo qui ma, come puoi vedere, abbiamo centralizzato parametri e metodi di rete comuni in un gestore singleton, che ha notevolmente semplificato le nostre implementazioni del controller di visualizzazione. Gli aggiornamenti futuri saranno semplici e veloci e, soprattutto, separano la nostra rete dall'esperienza dell'utente. La prossima volta che il team di progettazione chiederà una revisione dell'interfaccia utente/UX, sapremo che il nostro lavoro è già stato fatto per quanto riguarda il networking!

In questo articolo ci siamo concentrati su un singleton di rete, ma questi stessi principi potrebbero essere applicati a molte altre funzioni centralizzate come:

  • Gestione dello stato e delle autorizzazioni dell'utente
  • Instradamento delle azioni touch alla navigazione dell'app
  • Gestione video e audio
  • Analitica
  • Notifiche
  • periferiche
  • e molto altro ancora…

Ci siamo anche concentrati su un'architettura per applicazioni iOS, ma questa potrebbe essere facilmente estesa ad Android e persino a JavaScript. Come bonus, creando codice altamente definito e orientato alle funzioni, rende il porting di app su nuove piattaforme un'attività molto più rapida.

Per riassumere, dedicando un po' di tempo in più nella pianificazione iniziale del progetto per stabilire metodi singleton chiave, come l'esempio di rete sopra, il codice futuro può essere più pulito, più semplice e più gestibile.