Scentralizowana i oddzielona sieć iOS: samouczek AFNetworking z klasą Singleton

Opublikowany: 2022-03-11

Jeśli chodzi o wzorce architektury systemu iOS, wzorzec projektowy Model-View-Controller (MVC) doskonale nadaje się do długowieczności i łatwości konserwacji bazy kodu aplikacji. Umożliwia łatwe ponowne wykorzystanie lub wymianę klas w celu spełnienia różnych wymagań poprzez oddzielenie ich od siebie. Pomaga to zmaksymalizować zalety programowania zorientowanego obiektowo (OOP).

Chociaż ta architektura aplikacji na iOS działa dobrze na poziomie mikro (poszczególne ekrany/sekcje aplikacji), możesz dodawać podobne funkcje do wielu modeli w miarę rozwoju aplikacji. W przypadkach takich jak praca w sieci lepszym podejściem może być przeniesienie wspólnej logiki z klas modeli do klas pomocniczych singleton. W tym samouczku AFNetworking iOS nauczę Cię, jak skonfigurować scentralizowany pojedynczy obiekt sieciowy, który, oddzielony od komponentów MVC na poziomie mikro, może być ponownie wykorzystany w całej aplikacji o oddzielonej architekturze.

Samouczek AFNetworking: scentralizowana i oddzielona sieć z Singleton

Problem z siecią iOS

Apple wykonało świetną robotę, wyabstrahowując wiele złożoności zarządzania sprzętem mobilnym w łatwych w użyciu pakietach SDK iOS, ale w niektórych przypadkach, takich jak obsługa sieci, Bluetooth, OpenGL i przetwarzanie multimediów, klasy mogą być uciążliwe ze względu na ich cel elastyczne pakiety SDK. Na szczęście bogata społeczność programistów iOS stworzyła struktury wysokiego poziomu, aby uprościć większość typowych przypadków użycia, starając się uprościć projektowanie i strukturę aplikacji. Dobry programista, posługujący się najlepszymi praktykami architektury aplikacji ios, wie, jakich narzędzi użyć, po co ich używać i kiedy lepiej pisać własne narzędzia i klasy od podstaw.

AFNetworking to świetny przykład sieci i jeden z najczęściej używanych frameworków open source, który upraszcza codzienne zadania programisty. Upraszcza sieć RESTful API i tworzy modułowe wzorce żądań/odpowiedzi z blokami sukcesu, postępu i zakończenia niepowodzenia. Eliminuje to potrzebę wdrażania metod delegatów zaimplementowanych przez programistę i niestandardowych ustawień żądań/połączeń i można je bardzo szybko uwzględnić w dowolnej klasie.

Problem z AFNetworking

AFNetworking jest świetny, ale jego modułowość może również prowadzić do jego wykorzystania w sposób fragmentaryczny. Typowe nieefektywne implementacje mogą obejmować:

  • Wiele żądań sieciowych przy użyciu podobnych metod i właściwości w jednym kontrolerze widoku

  • Prawie identyczne żądania w wielu kontrolerach widoku, które prowadzą do rozproszonych wspólnych zmiennych, które mogą nie być zsynchronizowane

  • Żądania sieciowe w klasie dla danych niezwiązanych z tą klasą

W przypadku aplikacji z ograniczoną liczbą widoków, kilkoma wywołaniami interfejsu API do zaimplementowania i takimi, które prawdopodobnie nie będą się często zmieniać, może to nie być dużym problemem. Jednak bardziej prawdopodobne jest, że myślisz na wielką skalę i planujesz wiele lat aktualizacji. Jeśli twoja sprawa jest druga, prawdopodobnie będziesz musiał zająć się:

  • Wersjonowanie API w celu obsługi wielu generacji aplikacji

  • Dodawanie nowych parametrów lub zmiany istniejących parametrów w czasie w celu rozszerzenia możliwości

  • Implementacja zupełnie nowych API

Jeśli twój kod sieciowy jest rozproszony po całej bazie kodu, jest to teraz potencjalny koszmar. Miejmy nadzieję, że przynajmniej niektóre parametry są zdefiniowane statycznie we wspólnym nagłówku, ale nawet wtedy możesz dotknąć kilkunastu klas, aby wprowadzić nawet najmniejsze zmiany.

Jak radzimy sobie z ograniczeniami AFNetworking?

Utwórz singleton sieciowy, aby scentralizować obsługę żądań, odpowiedzi i ich parametrów.

Obiekt singleton zapewnia globalny punkt dostępu do zasobów swojej klasy. Singletony są używane w sytuacjach, w których ten pojedynczy punkt kontroli jest pożądany, na przykład w przypadku klas oferujących jakąś ogólną usługę lub zasoby. Uzyskujesz globalną instancję z klasy singleton za pomocą metody fabryki. - Jabłko

Tak więc singleton jest klasą, której można mieć tylko jedną instancję w aplikacji, która istnieje przez cały czas jej istnienia. Dodatkowo, ponieważ wiemy, że istnieje tylko jedna instancja, jest ona łatwo dostępna dla każdej innej klasy, która potrzebuje dostępu do jej metod lub właściwości.

Oto dlaczego powinniśmy używać singletona do pracy w sieci:

  • Jest inicjowany statycznie, więc po utworzeniu będzie miał te same metody i właściwości dostępne dla każdej klasy, która próbuje uzyskać do niego dostęp. Nie ma szans na dziwne problemy z synchronizacją lub żądanie danych z niewłaściwej instancji klasy.

  • Możesz ograniczyć swoje wywołania API, aby nie przekraczały limitu szybkości (np. gdy musisz utrzymać żądania API poniżej pięciu na sekundę).

  • Właściwości statyczne, takie jak nazwa hosta, numery portów, punkty końcowe, wersja interfejsu API, typ urządzenia, trwałe identyfikatory, rozmiar ekranu itp. mogą być umieszczone wspólnie, dzięki czemu jedna zmiana wpływa na wszystkie żądania sieciowe.

  • Wspólne właściwości mogą być ponownie wykorzystywane w wielu żądaniach sieciowych.

  • Obiekt singleton nie zajmuje pamięci, dopóki nie zostanie utworzona instancja. Może to być przydatne w przypadku singli o bardzo specyficznych przypadkach użycia, których niektórzy użytkownicy mogą nigdy nie potrzebować, takich jak obsługa przesyłania wideo na Chromecasta, jeśli nie mają urządzenia.

  • Żądania sieciowe mogą być całkowicie oddzielone od widoków i kontrolerów, dzięki czemu mogą być kontynuowane nawet po zniszczeniu widoków i kontrolerów.

  • Rejestrowanie w sieci może być scentralizowane i uproszczone.

  • Typowe zdarzenia niepowodzenia, takie jak alerty, można ponownie wykorzystać do wszystkich żądań.

  • Główna struktura takiego singletona może być ponownie wykorzystana w wielu projektach z prostymi zmianami właściwości statycznych najwyższego poziomu.

Kilka powodów, aby nie używać singletonów:

  • Mogą być nadużywane, aby zapewnić wiele obowiązków w jednej klasie. Na przykład metody przetwarzania wideo można mieszać z metodami sieciowymi lub metodami stanu użytkownika. Byłoby to prawdopodobnie kiepską praktyką projektową i prowadziłoby do trudnego do zrozumienia kodu. Zamiast tego należy stworzyć wiele singletonów z określonymi obowiązkami.

  • Singletony nie mogą być dzielone na podklasy.

  • Singletony mogą ukrywać zależności, przez co stają się mniej modułowe. Na przykład, jeśli singleton zostanie usunięty, a klasie brakowało importu, który zaimportował singleton, może to prowadzić do przyszłych problemów (zwłaszcza jeśli istnieją zależności bibliotek zewnętrznych).

  • Klasa może modyfikować wspólne właściwości w singletonach podczas długich operacji, które są nieoczekiwane w innej klasie. Bez odpowiedniego zastanowienia się nad tym wyniki mogą się różnić.

  • Wycieki pamięci w singletonie mogą stać się poważnym problemem, ponieważ sam singleton nigdy nie jest dealokowany.

Jednak korzystając z najlepszych praktyk dotyczących architektury aplikacji na iOS, te negatywy można złagodzić. Oto kilka najlepszych praktyk:

  • Każdy singleton powinien ponosić pojedynczą odpowiedzialność.

  • Nie używaj singletonów do przechowywania danych, które będą szybko zmieniane przez wiele klas lub wątków, jeśli potrzebujesz wysokiej dokładności.

  • Twórz singletony, aby włączać/wyłączać funkcje w oparciu o dostępne zależności.

  • Nie przechowuj dużych ilości danych we właściwościach pojedynczych, ponieważ będą one utrwalane przez cały okres użytkowania aplikacji (chyba że są zarządzane ręcznie).

Prosty przykład singletona z AFNetworking

Najpierw, jako warunek wstępny, dodaj AFNetworking do swojego projektu. Najprostszym podejściem jest użycie Cocoapods, a instrukcje znajdują się na jego stronie GitHub.

Skoro już przy tym jesteś, sugeruję dodanie UIAlertController+Blocks i MBProgressHUD (znowu łatwo dodawane z CocoaPods). Są one oczywiście opcjonalne, ale znacznie uprości to postęp i alerty, jeśli chcesz je zaimplementować w pojedynczym oknie AppDelegate.

Po AFNetworking zacznij od utworzenia nowej klasy Cocoa Touch o nazwie NetworkManager jako podklasy NSObject . Dodaj metodę klasy dostępu do menedżera. Twój plik NetworkManager.h powinien wyglądać jak poniższy kod:

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

Następnie zaimplementuj podstawowe metody inicjalizacji dla singletona i zaimportuj nagłówek AFNetworking. Twoja implementacja klasy powinna wyglądać następująco (UWAGA: zakłada się, że używasz automatycznego zliczania referencji):

 #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

Świetnie! Teraz gotujemy i jesteśmy gotowi do dodania właściwości i metod. Jako szybki test, aby zrozumieć, jak uzyskać dostęp do singletona, dodajmy następujące elementy do NetworkManager.h :

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

Oraz następujące do 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); }

Następnie w naszym głównym pliku ViewController.m (lub cokolwiek masz), zaimportuj NetworkManager.h , a następnie w viewDidLoad dodaj:

 [[NetworkManager sharedManager] test];

Uruchom aplikację i powinieneś zobaczyć następujące dane wyjściowe:

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

Ok, więc prawdopodobnie nie będziesz mieszać #define , static const i @property jednocześnie w ten sposób, ale po prostu pokazując dla jasności swoje opcje. „static const” jest lepszą deklaracją bezpieczeństwa typów, ale #define może być przydatne w tworzeniu ciągów, ponieważ pozwala na używanie makr. Co to jest warte, używam #define dla zwięzłości w tym scenariuszu. O ile nie używasz wskaźników, w praktyce nie ma dużej różnicy między tymi podejściami do deklaracji.

Teraz, gdy rozumiesz #defines , stałe, właściwości i metody, możemy je usunąć i przejść do bardziej odpowiednich przykładów.

Przykład sieci

Wyobraź sobie aplikację, w której użytkownik musi być zalogowany, aby uzyskać dostęp do czegokolwiek. Po uruchomieniu aplikacji sprawdzimy, czy zapisaliśmy token uwierzytelniający, a jeśli tak, wykonamy żądanie GET do naszego interfejsu API, aby sprawdzić, czy token wygasł, czy nie.

W AppDelegate.m zarejestrujmy domyślny dla naszego tokena:

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

Dodamy czek tokena do Menedżera sieci i otrzymamy informację zwrotną na temat czeku za pomocą bloków uzupełniania. Możesz zaprojektować te bloki uzupełniające, jak chcesz. W tym przykładzie używam sukcesu z danymi obiektu odpowiedzi i niepowodzenia z ciągiem odpowiedzi błędu i kodem stanu. Uwaga: Awarię można opcjonalnie pominąć, jeśli nie ma to znaczenia dla strony otrzymującej, na przykład zwiększanie wartości w analizie.

NetworkManager.h

Powyżej @interface :

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

W @interfejsie:

@property (nonatomic, strong) AFHTTPSessionManager *networkingManager;

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

NetworkManager.m:

Zdefiniuj nasz 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]

Dodamy kilka metod pomocniczych, aby uprościć uwierzytelnione żądania, a także błędy analizy (w tym przykładzie zastosowano token sieciowy 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"; }

Jeśli dodałeś MBProgressHUD, możesz go użyć tutaj:

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

A nasza prośba o sprawdzenie tokena:

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

Teraz w metodzie ViewController.m viewWillAppear wywołamy tę metodę singleton. Zwróć uwagę na prostotę żądania i małą implementację po stronie kontrolera widoku.

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

Otóż ​​to! Zwróć uwagę, jak ten fragment kodu może być użyty praktycznie w dowolnej aplikacji, która musi sprawdzać uwierzytelnianie podczas uruchamiania.

Podobnie możemy obsłużyć żądanie logowania POST: 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); } } }

Możemy tutaj wymyślić i dodać alerty za pomocą AlertController+Blocks w oknie AppDelegate lub po prostu wysłać obiekty niepowodzeń z powrotem do kontrolera widoku. Dodatkowo moglibyśmy zapisać tutaj poświadczenia użytkownika lub zamiast tego pozwolić kontrolerowi widoku obsłużyć to. Zazwyczaj implementuję oddzielny singleton UserManager, który obsługuje poświadczenia i uprawnienia, które mogą komunikować się bezpośrednio z NetworkManagerem (preferencje osobiste).

Po raz kolejny strona kontrolera widoku jest bardzo prosta:

 - (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! Zapomnieliśmy o wersji API i wysłaniu typu urządzenia. Dodatkowo zaktualizowaliśmy punkt końcowy z „/checktoken” na „/token”. Ponieważ scentralizowaliśmy naszą sieć, aktualizacja jest bardzo łatwa. Nie musimy przekopywać się przez nasz kod. Ponieważ będziemy używać tych parametrów we wszystkich żądaniach, utworzymy pomocnika.

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

W przyszłości można łatwo dodać do tego dowolną liczbę wspólnych parametrów. Następnie możemy zaktualizować nasze metody sprawdzania tokena i uwierzytelnienia, takie jak:

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

Podsumowanie naszego samouczka AFNetworking

Na tym się zatrzymamy, ale jak widać, scentralizowaliśmy wspólne parametry i metody sieciowe w pojedynczym menedżerze, co znacznie uprościło nasze implementacje kontrolera widoku. Przyszłe aktualizacje będą proste i szybkie, a co najważniejsze, oddzielą naszą sieć od doświadczenia użytkownika. Następnym razem, gdy zespół projektowy poprosi o przegląd UI/UX, będziemy wiedzieć, że nasza praca po stronie sieciowej jest już wykonana!

W tym artykule skupiliśmy się na singletonie sieciowym, ale te same zasady można zastosować do wielu innych scentralizowanych funkcji, takich jak:

  • Obsługa stanu i uprawnień użytkownika
  • Kierowanie działań dotykowych do nawigacji w aplikacji
  • Zarządzanie wideo i dźwiękiem
  • Analityka
  • Powiadomienia
  • Urządzenia peryferyjne
  • i dużo dużo więcej…

Skupiliśmy się również na architekturze aplikacji iOS, ale równie łatwo można ją rozszerzyć na Androida, a nawet JavaScript. Jako bonus, tworząc wysoce zdefiniowany i zorientowany na funkcje kod, znacznie przyspiesza przenoszenie aplikacji na nowe platformy.

Podsumowując, spędzając trochę więcej czasu na wczesnym planowaniu projektu w celu ustalenia kluczowych pojedynczych metod, jak w powyższym przykładzie sieciowym, Twój przyszły kod może być czystszy, prostszy i łatwiejszy w utrzymaniu.