Rețea centralizată și decuplată iOS: Tutorial AFNetworking cu o clasă Singleton

Publicat: 2022-03-11

Când vine vorba de modele de arhitectură iOS, modelul de design Model-View-Controller (MVC) este excelent pentru longevitatea și mentenabilitatea bazei de cod a unei aplicații. Permite reutilizarea sau înlocuirea cu ușurință a claselor pentru a susține diverse cerințe prin decuplarea lor unele de altele. Acest lucru ajută la maximizarea avantajelor programării orientate pe obiecte (OOP).

În timp ce această arhitectură a aplicației iOS funcționează bine la nivel micro (ecranele/secțiunile individuale ale unei aplicații), este posibil să adăugați funcții similare la mai multe modele pe măsură ce aplicația dvs. crește. În cazuri precum crearea de rețele, mutarea logicii comune din clasele model și în clasele de ajutor singleton poate fi o abordare mai bună. În acest tutorial iOS AFNetworking, vă voi învăța cum să configurați un obiect de rețea singleton centralizat care, decuplat de componentele MVC la nivel micro, poate fi reutilizat în aplicația dvs. de arhitectură decuplată.

Tutorial AFNetworking: Rețea centralizată și decuplată cu Singleton

Problema cu rețeaua iOS

Apple a făcut o treabă grozavă de a abstractiza multe dintre complexitățile gestionării hardware-ului mobil în SDK-uri iOS ușor de utilizat, dar în unele cazuri, cum ar fi rețelele, Bluetooth, OpenGL și procesarea multimedia, clasele pot fi greoaie datorită obiectivului lor de a păstra SDK-urile flexibile. Din fericire, bogata comunitate de dezvoltatori iOS a creat cadre la nivel înalt pentru a simplifica cele mai frecvente cazuri de utilizare într-un efort de a simplifica designul și structura aplicațiilor. Un programator bun, care folosește cele mai bune practici privind arhitectura aplicației iOS, știe ce instrumente să folosească, de ce să le folosească și când este mai bine să vă scrieți propriile instrumente și clase de la zero.

AFNetworking este un exemplu grozav de rețea și unul dintre cadrele open source cele mai frecvent utilizate, care simplifică sarcinile de zi cu zi ale dezvoltatorului. Simplifică rețeaua API RESTful și creează modele modulare de solicitare/răspuns cu blocuri de finalizare cu succes, progres și eșec. Acest lucru elimină nevoia de metode de delegare implementate de dezvoltator și setări personalizate de solicitare/conectare și pot fi incluse în orice clasă foarte rapid.

Problema cu AFNetworking

AFNetworking este grozav, dar modularitatea sa poate duce și la utilizarea sa în moduri fragmentate. Implementările comune ineficiente pot include:

  • Mai multe solicitări de rețea folosind metode și proprietăți similare într-un singur controler de vizualizare

  • Solicitări aproape identice în controlere de vizualizare multiple care duc la variabile comune distribuite care se pot desincroniza

  • Solicitări de rețea într-o clasă pentru date care nu au legătură cu acea clasă

Pentru aplicațiile cu un număr limitat de vizualizări, puține apeluri API de implementat și cele care probabil că nu se vor schimba des, acest lucru poate să nu fie de mare îngrijorare. Cu toate acestea, este mai probabil să vă gândiți la mare și să aveți mulți ani de actualizări planificate. Dacă cazul tău este cel din urmă, probabil că vei avea nevoie să te ocupi de:

  • Versiune API pentru a suporta mai multe generații ale unei aplicații

  • Adăugarea de noi parametri sau modificări ale parametrilor existenți în timp pentru a extinde capacitatea

  • Implementarea de API-uri complet noi

Dacă codul dvs. de rețea este împrăștiat în întreaga bază de cod, acesta este acum un potențial coșmar. Sperăm că aveți cel puțin unii dintre parametrii definiți static într-un antet comun, dar chiar și atunci puteți atinge o duzină de clase chiar și pentru cele mai mici modificări.

Cum abordăm limitările AFNetworking?

Creați un singleton de rețea pentru a centraliza gestionarea cererilor, a răspunsurilor și a parametrilor acestora.

Un obiect singleton oferă un punct global de acces la resursele clasei sale. Singleton-urile sunt folosite în situațiile în care acest punct unic de control este de dorit, cum ar fi cu clasele care oferă un serviciu sau o resursă generală. Obțineți instanța globală dintr-o clasă singleton printr-o metodă din fabrică. - Măr

Deci, un singleton este o clasă din care ați avea o singură instanță într-o aplicație care există pe toată durata de viață a aplicației. În plus, deoarece știm că există o singură instanță, aceasta este ușor accesibilă de către orice altă clasă care trebuie să-și acceseze metodele sau proprietățile.

Iată de ce ar trebui să folosim un singleton pentru rețea:

  • Este inițializat static, astfel încât, odată creat, va avea aceleași metode și proprietăți disponibile oricărei clase care încearcă să-l acceseze. Nu există nicio șansă pentru probleme ciudate de sincronizare sau solicitarea de date de la o instanță greșită a unei clase.

  • Puteți limita apelurile API pentru a rămâne sub o limită de rată (de exemplu, atunci când trebuie să păstrați solicitările API sub cinci pe secundă).

  • Proprietățile statice, cum ar fi numele gazdei, numerele de port, punctele finale, versiunea API, tipul dispozitivului, ID-urile persistente, dimensiunea ecranului etc. pot fi localizate în comun, astfel încât o singură modificare să afecteze toate solicitările de rețea.

  • Proprietățile comune pot fi reutilizate între multe solicitări de rețea.

  • Obiectul singleton nu ocupă memorie până când nu este instanțiat. Acest lucru poate fi util pentru singleton cu cazuri de utilizare foarte specifice de care unii utilizatori nu au nevoie niciodată, cum ar fi gestionarea difuzării de videoclipuri pe un Chromecast dacă nu au dispozitivul.

  • Solicitările de rețea pot fi complet decuplate de vizualizări și controlere, astfel încât să poată continua chiar și după ce vizualizările și controlerele sunt distruse.

  • Înregistrarea în rețea poate fi centralizată și simplificată.

  • Evenimentele comune pentru eșec, cum ar fi alertele, pot fi reutilizate pentru toate solicitările.

  • Structura principală a unui astfel de singleton ar putea fi reutilizată pe mai multe proiecte cu modificări simple ale proprietăților statice de nivel superior.

Câteva motive pentru a nu folosi singletons:

  • Ele pot fi suprautilizate pentru a oferi mai multe responsabilități într-o singură clasă. De exemplu, metodele de procesare video ar putea fi amestecate cu metodele de rețea sau cu metodele de stare a utilizatorului. Aceasta ar fi probabil o practică proastă de proiectare și ar duce la un cod greu de înțeles. În schimb, ar trebui create mai multe singleton-uri cu responsabilități specifice.

  • Singletons nu pot fi subclasate.

  • Singleton-urile pot ascunde dependențele și astfel devin mai puțin modulare. De exemplu, dacă un singleton este eliminat și o clasă lipsește un import pe care singleton-ul l-a importat, poate duce la probleme viitoare (mai ales dacă există dependențe de bibliotecă externă).

  • O clasă poate modifica proprietățile partajate în singletons în timpul operațiunilor lungi care sunt neașteptate într-o altă clasă. Fără o gândire adecvată la acest lucru, rezultatele pot varia.

  • Scurgerile de memorie într-un singleton pot deveni o problemă semnificativă, deoarece singletonul în sine nu este niciodată de-alocat.

Cu toate acestea, folosind cele mai bune practici pentru arhitectura aplicației iOS, aceste aspecte negative pot fi atenuate. Câteva bune practici includ:

  • Fiecare singleton ar trebui să se ocupe de o singură responsabilitate.

  • Nu utilizați singleton-uri pentru a stoca date care vor fi modificate rapid de mai multe clase sau fire de execuție dacă aveți nevoie de o precizie ridicată.

  • Creați singletonuri pentru a activa/dezactiva funcțiile pe baza dependențelor disponibile.

  • Nu stocați cantități mari de date în proprietăți singleton, deoarece acestea vor persista pe toată durata de viață a aplicației dvs. (cu excepția cazului în care sunt gestionate manual).

Un exemplu simplu Singleton cu AFNetworking

În primul rând, ca o condiție prealabilă, adăugați AFNetworking la proiectul dvs. Cea mai simplă abordare este prin Cocoapods, iar instrucțiunile se găsesc pe pagina GitHub.

În timp ce sunteți la asta, aș sugera să adăugați UIAlertController+Blocks și MBProgressHUD (din nou ușor adăugat cu CocoaPods). Acestea sunt în mod clar opționale, dar acest lucru va simplifica foarte mult progresul și alertele dacă doriți să le implementați în singletonul din fereastra AppDelegate.

Odată ce AFNetworking este adăugat, începeți prin a crea o nouă clasă Cocoa Touch numită NetworkManager ca subclasă a NSObject . Adăugați o metodă de clasă pentru accesarea managerului. Fișierul dvs. NetworkManager.h ar trebui să arate ca codul de mai jos:

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

Apoi, implementați metodele de inițializare de bază pentru singleton și importați antetul AFNetworking. Implementarea clasei dvs. ar trebui să arate astfel (NOTĂ: se presupune că utilizați Contorizarea automată a referințelor):

 #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

Grozav! Acum gătim și gata să adăugăm proprietăți și metode. Ca test rapid pentru a înțelege cum să accesați un singleton, să adăugăm următoarele la NetworkManager.h :

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

Și următoarele pentru 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); }

Apoi, în fișierul nostru principal ViewController.m (sau orice aveți), importați NetworkManager.h și apoi în viewDidLoad adăugați:

 [[NetworkManager sharedManager] test];

Lansați aplicația și ar trebui să vedeți următoarele în rezultat:

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

Ok, deci probabil că nu veți amesteca #define , static const și @property dintr-o dată astfel, ci pur și simplu arătând pentru claritate opțiunile dvs. „static const” este o declarație mai bună pentru siguranța tipului, dar #define poate fi util în construirea șirurilor, deoarece permite utilizarea macrocomenzilor. Pentru ceea ce merită, folosesc #define pentru concizie în acest scenariu. Cu excepția cazului în care utilizați pointeri, nu există prea multe diferențe în practică între aceste abordări de declarare.

Acum că înțelegeți #defines , constantele, proprietățile și metodele, le putem elimina și trece la exemple mai relevante.

Un exemplu de rețea

Imaginați-vă o aplicație în care utilizatorul trebuie să fie conectat pentru a accesa orice. La lansarea aplicației, vom verifica dacă am salvat un token de autentificare și, dacă da, vom efectua o solicitare GET către API-ul nostru pentru a vedea dacă tokenul a expirat sau nu.

În AppDelegate.m , să înregistrăm o valoare implicită pentru simbolul nostru:

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

Vom adăuga o verificare simbol la NetworkManager și vom primi feedback cu privire la verificare prin blocuri de finalizare. Puteți proiecta aceste blocuri de completare așa cum doriți. În acest exemplu, folosesc succesul cu datele obiectului de răspuns și eșecul cu șirul de răspuns de eroare și un cod de stare. Notă: Eșecul poate fi exclus opțional dacă nu contează pentru partea de recepție, cum ar fi creșterea unei valori în analiză.

NetworkManager.h

Deasupra @interface :

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

În @interfață:

@proprietate (nonatomică, puternică) AFHTTPSessionManager *networkingManager;

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

NetworkManager.m:

Definiți BASE_URL-ul nostru:

 #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]

Vom adăuga câteva metode de ajutor pentru a simplifica cererile autentificate, precum și erorile de analiză (acest exemplu folosește 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"; }

Dacă ați adăugat MBProgressHUD, acesta poate fi folosit aici:

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

Și cererea noastră de verificare a simbolurilor:

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

Acum, în metoda ViewController.m viewWillAppear, vom numi această metodă singleton. Observați simplitatea cererii și implementarea minusculă din partea 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]; }];

Asta e! Observați cum acest fragment ar putea fi utilizat practic în orice aplicație care trebuie să verifice autentificarea la lansare.

În mod similar, putem gestiona o solicitare POST pentru autentificare: 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); } } }

Ne putem încânta aici și putem adăuga alerte cu AlertController+Blocks în fereastra AppDelegate sau pur și simplu trimitem obiectele de eșec înapoi la controlerul de vizualizare. În plus, am putea salva acreditările utilizatorului aici sau, în schimb, lăsăm controlerul de vizualizare să se ocupe de asta. De obicei, implementez un singleton UserManager separat care gestionează acreditările și permisiunile care pot comunica direct cu NetworkManager (preferință personală).

Încă o dată, partea controlerului de vizualizare este super simplă:

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

Hopa! Am uitat să versăm API-ul și să trimitem tipul dispozitivului. În plus, am actualizat punctul final de la „/checktoken” la „/token”. Deoarece ne-am centralizat rețeaua, aceasta este foarte ușor de actualizat. Nu trebuie să căpătăm codul nostru. Deoarece vom folosi acești parametri pentru toate solicitările, vom crea un ajutor.

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

Orice număr de parametri comuni pot fi adăugați cu ușurință la acest lucru în viitor. Apoi ne putem actualiza metodele de verificare și autentificare a simbolurilor, astfel:

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

Încheierea tutorialului nostru AFNetworking

Ne vom opri aici, dar, după cum puteți vedea, am centralizat parametrii și metodele comuni de rețea într-un manager singleton, ceea ce a simplificat foarte mult implementările noastre de controler de vizualizare. Actualizările viitoare vor fi simple și rapide și, cel mai important, decuplează rețeaua noastră de experiența utilizatorului. Data viitoare când echipa de proiectare va cere o revizuire a UI/UX, vom ști că treaba noastră este deja făcută pe partea de rețea!

În acest articol ne-am concentrat pe un singleton de rețea, dar aceleași principii ar putea fi aplicate multor alte funcții centralizate, cum ar fi:

  • Gestionarea stării utilizatorului și a permisiunilor
  • Dirijarea acțiunilor tactile către navigarea în aplicație
  • Management video și audio
  • Analytics
  • Notificări
  • Periferice
  • si mult mai multe...

Ne-am concentrat și pe o arhitectură de aplicație iOS, dar aceasta ar putea fi la fel de ușor extinsă la Android și chiar la JavaScript. Ca bonus, prin crearea unui cod foarte definit și orientat pe funcție, face ca portarea aplicațiilor pe noi platforme să fie o sarcină mult mai rapidă.

Pentru a rezuma, petrecând puțin timp în plus în planificarea timpurie a proiectului pentru a stabili metode singleton cheie, cum ar fi exemplul de rețea de mai sus, viitorul cod poate fi mai curat, mai simplu și mai ușor de întreținut.