Zentralisiertes und entkoppeltes iOS-Netzwerk: AFNetworking-Lernprogramm mit einer Singleton-Klasse
Veröffentlicht: 2022-03-11Wenn es um iOS-Architekturmuster geht, ist das Model-View-Controller (MVC)-Entwurfsmuster großartig für die Langlebigkeit und Wartbarkeit der Codebasis einer Anwendung. Klassen können leicht wiederverwendet oder ersetzt werden, um verschiedene Anforderungen zu unterstützen, indem sie voneinander entkoppelt werden. Dies trägt dazu bei, die Vorteile der objektorientierten Programmierung (OOP) zu maximieren.
Während diese iOS-Anwendungsarchitektur auf der Mikroebene (einzelne Bildschirme/Abschnitte einer App) gut funktioniert, fügen Sie möglicherweise mehreren Modellen ähnliche Funktionen hinzu, wenn Ihre App wächst. In Fällen wie Netzwerken kann es ein besserer Ansatz sein, allgemeine Logik aus Ihren Modellklassen in Singleton-Hilfsklassen zu verschieben. In diesem AFNetworking iOS-Lernprogramm zeige ich Ihnen, wie Sie ein zentralisiertes Singleton-Netzwerkobjekt einrichten, das, entkoppelt von MVC-Komponenten auf Mikroebene, in Ihrer entkoppelten Architekturanwendung wiederverwendet werden kann.
Das Problem mit iOS-Netzwerken
Apple hat großartige Arbeit geleistet, indem es viele der Komplexitäten der Verwaltung mobiler Hardware in einfach zu verwendenden iOS-SDKs abstrahiert hat, aber in einigen Fällen, wie z die SDKs flexibel. Zum Glück hat die reiche iOS-Entwicklergemeinschaft hochrangige Frameworks erstellt, um die häufigsten Anwendungsfälle zu vereinfachen, um das Design und die Struktur von Anwendungen zu vereinfachen. Ein guter Programmierer, der Best Practices für die Architektur von iOS-Apps anwendet, weiß, welche Tools zu verwenden sind, warum man sie verwendet und wann es besser ist, eigene Tools und Klassen von Grund auf neu zu schreiben.
AFNetworking ist ein großartiges Netzwerkbeispiel und eines der am häufigsten verwendeten Open-Source-Frameworks, das die täglichen Aufgaben eines Entwicklers vereinfacht. Es vereinfacht die RESTful-API-Vernetzung und erstellt modulare Anforderungs-/Antwortmuster mit Erfolgs-, Fortschritts- und Fehlerabschlussblöcken. Dadurch werden vom Entwickler implementierte Delegate-Methoden und benutzerdefinierte Anforderungs-/Verbindungseinstellungen überflüssig und können sehr schnell in jede Klasse aufgenommen werden.
Das Problem mit AFNetworking
AFNetworking ist großartig, aber seine Modularität kann auch zu einer fragmentierten Verwendung führen. Häufige ineffiziente Implementierungen können Folgendes umfassen:
Mehrere Netzwerkanforderungen mit ähnlichen Methoden und Eigenschaften in einem einzigen Ansichtscontroller
Nahezu identische Anforderungen in mehreren View-Controllern, die zu verteilten gemeinsamen Variablen führen, die nicht mehr synchron sein können
Netzwerkanfragen in einer Klasse nach Daten, die nichts mit dieser Klasse zu tun haben
Für Anwendungen mit einer begrenzten Anzahl von Ansichten, wenigen zu implementierenden API-Aufrufen und solchen, die sich wahrscheinlich nicht oft ändern, ist dies möglicherweise kein großes Problem. Wahrscheinlicher ist jedoch, dass Sie groß denken und viele Jahre lang Updates geplant haben. Wenn Ihr Fall letzteres ist, müssen Sie sich wahrscheinlich mit Folgendem befassen:
API-Versionierung zur Unterstützung mehrerer Generationen einer Anwendung
Hinzufügen neuer Parameter oder Änderungen an bestehenden Parametern im Laufe der Zeit, um die Möglichkeiten zu erweitern
Implementierung völlig neuer APIs
Wenn Ihr Netzwerkcode über Ihre gesamte Codebasis verstreut ist, ist dies jetzt ein potenzieller Albtraum. Hoffentlich haben Sie zumindest einige Ihrer Parameter statisch in einem gemeinsamen Header definiert, aber selbst dann können Sie ein Dutzend Klassen für selbst die kleinsten Änderungen berühren.
Wie gehen wir mit AFNetworking-Einschränkungen um?
Erstellen Sie ein Netzwerk-Singleton, um die Verarbeitung von Anforderungen, Antworten und deren Parametern zu zentralisieren.
Ein Singleton-Objekt bietet einen globalen Zugriffspunkt auf die Ressourcen seiner Klasse. Singletons werden in Situationen verwendet, in denen dieser einzelne Kontrollpunkt wünschenswert ist, z. B. bei Klassen, die einen allgemeinen Dienst oder eine Ressource anbieten. Sie erhalten die globale Instanz von einer Singleton-Klasse über eine Factory-Methode. - Apfel
Ein Singleton ist also eine Klasse, von der Sie in einer Anwendung immer nur eine Instanz haben würden, die für die Lebensdauer der Anwendung existiert. Da wir wissen, dass es nur eine Instanz gibt, ist sie für jede andere Klasse, die auf ihre Methoden oder Eigenschaften zugreifen muss, leicht zugänglich.
Hier ist, warum wir einen Singleton für das Netzwerk verwenden sollten:
Es wird statisch initialisiert, sodass nach seiner Erstellung dieselben Methoden und Eigenschaften für jede Klasse verfügbar sind, die versucht, darauf zuzugreifen. Es gibt keine Chance für seltsame Synchronisationsprobleme oder das Anfordern von Daten von der falschen Instanz einer Klasse.
Sie können Ihre API-Aufrufe so begrenzen, dass sie unter einem Ratenlimit bleiben (z. B. wenn Sie Ihre API-Anfragen unter fünf pro Sekunde halten müssen).
Statische Eigenschaften wie Hostname, Portnummern, Endpunkte, API-Version, Gerätetyp, dauerhafte IDs, Bildschirmgröße usw. können zusammengelegt werden, sodass sich eine Änderung auf alle Netzwerkanforderungen auswirkt.
Gemeinsame Eigenschaften können zwischen vielen Netzwerkanforderungen wiederverwendet werden.
Das Singleton-Objekt belegt keinen Speicher, bis es instanziiert wird. Dies kann für Singletons mit sehr spezifischen Anwendungsfällen nützlich sein, die einige Benutzer möglicherweise nie benötigen, z. B. das Übertragen von Videos auf einen Chromecast, wenn sie nicht über das Gerät verfügen.
Netzwerkanforderungen können vollständig von Ansichten und Controllern entkoppelt werden, sodass sie auch nach der Zerstörung von Ansichten und Controllern fortgesetzt werden können.
Die Netzwerkprotokollierung kann zentralisiert und vereinfacht werden.
Häufige Fehlerereignisse wie Warnungen können für alle Anforderungen wiederverwendet werden.
Die Hauptstruktur eines solchen Singletons könnte in mehreren Projekten mit einfachen statischen Eigenschaftsänderungen auf oberster Ebene wiederverwendet werden.
Einige Gründe, Singletons nicht zu verwenden:
Sie können überbeansprucht werden, um mehrere Verantwortlichkeiten in einer einzigen Klasse bereitzustellen. Beispielsweise könnten Videoverarbeitungsmethoden mit Netzwerkmethoden oder User-State-Methoden gemischt werden. Dies wäre wahrscheinlich eine schlechte Entwurfspraxis und würde zu schwer verständlichem Code führen. Stattdessen sollten mehrere Singletons mit bestimmten Verantwortlichkeiten erstellt werden.
Singletons können nicht in Unterklassen unterteilt werden.
Singletons können Abhängigkeiten verbergen und werden dadurch weniger modular. Wenn beispielsweise ein Singleton entfernt wird und einer Klasse ein Import fehlt, den der Singleton importiert hat, kann dies zu zukünftigen Problemen führen (insbesondere wenn externe Bibliotheksabhängigkeiten vorhanden sind).
Eine Klasse kann gemeinsame Eigenschaften in Singletons während langer Operationen ändern, die in einer anderen Klasse unerwartet sind. Ohne gründliche Überlegung können die Ergebnisse variieren.
Speicherlecks in einem Singleton können zu einem erheblichen Problem werden, da das Singleton selbst nie freigegeben wird.
Durch die Verwendung von Best Practices für die iOS-App-Architektur können diese Nachteile jedoch gemildert werden. Einige Best Practices sind:
Jeder Singleton sollte eine einzelne Verantwortung übernehmen.
Verwenden Sie keine Singletons zum Speichern von Daten, die schnell von mehreren Klassen oder Threads geändert werden, wenn Sie eine hohe Genauigkeit benötigen.
Erstellen Sie Singletons, um Funktionen basierend auf verfügbaren Abhängigkeiten zu aktivieren/deaktivieren.
Speichern Sie keine großen Datenmengen in Singleton-Eigenschaften, da sie für die Lebensdauer Ihrer Anwendung bestehen bleiben (sofern sie nicht manuell verwaltet werden).
Ein einfaches Singleton-Beispiel mit AFNetworking
Fügen Sie als Voraussetzung zunächst AFNetworking zu Ihrem Projekt hinzu. Der einfachste Ansatz ist über Cocoapods und Anweisungen finden Sie auf der GitHub-Seite.
Wenn Sie schon dabei sind, würde ich vorschlagen, UIAlertController+Blocks
und MBProgressHUD
(wieder einfach mit CocoaPods hinzugefügt). Diese sind eindeutig optional, aber dies vereinfacht den Fortschritt und die Warnungen erheblich, falls Sie sie im Singleton im AppDelegate-Fenster implementieren möchten.
Nachdem AFNetworking
hinzugefügt wurde, erstellen Sie zunächst eine neue Cocoa Touch-Klasse mit dem Namen NetworkManager
als Unterklasse von NSObject
. Fügen Sie eine Klassenmethode für den Zugriff auf den Manager hinzu. Ihre NetworkManager.h
-Datei sollte wie im folgenden Code aussehen:
#import <Foundation/Foundation.h> #import “AFNetworking.h” @interface NetworkManager : NSObject + (id)sharedManager; @end
Implementieren Sie als Nächstes die grundlegenden Initialisierungsmethoden für den Singleton und importieren Sie den AFNetworking-Header. Ihre Klassenimplementierung sollte wie folgt aussehen (HINWEIS: Dies setzt voraus, dass Sie die automatische Referenzzählung verwenden):
#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
Toll! Jetzt kochen wir und sind bereit, Eigenschaften und Methoden hinzuzufügen. Als schnellen Test, um zu verstehen, wie man auf einen Singleton zugreift, fügen wir Folgendes zu NetworkManager.h
hinzu:
@property NSString *appID; - (void)test;
Und das Folgende zu 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); }
Importieren Sie dann in unserer Hauptdatei ViewController.m
(oder was auch immer Sie haben) NetworkManager.h
und fügen Sie dann in viewDidLoad
hinzu:
[[NetworkManager sharedManager] test];
Starten Sie die App und Sie sollten Folgendes in der Ausgabe sehen:
Testing our the networking singleton for appID: 1, HOST: http://www.apitesting.dev/, and PORT: 80
Ok, also werden Sie #define
, static const und @property
wahrscheinlich nicht auf einmal so mischen, sondern nur der Übersichtlichkeit halber Ihre Optionen zeigen. „static const“ ist eine bessere Deklaration für die Typsicherheit, aber #define
kann beim Erstellen von Zeichenfolgen nützlich sein, da es die Verwendung von Makros ermöglicht. Für das, was es wert ist, verwende ich in diesem Szenario #define
der Kürze halber. Sofern Sie keine Zeiger verwenden, gibt es in der Praxis keinen großen Unterschied zwischen diesen Deklarationsansätzen.
Nachdem Sie #defines
, Konstanten, Eigenschaften und Methoden verstanden haben, können wir diese entfernen und zu relevanteren Beispielen übergehen.

Ein Netzwerkbeispiel
Stellen Sie sich eine Anwendung vor, bei der der Benutzer angemeldet sein muss, um auf alles zugreifen zu können. Beim Start der App prüfen wir, ob wir ein Authentifizierungstoken gespeichert haben, und führen in diesem Fall eine GET-Anforderung an unsere API aus, um festzustellen, ob das Token abgelaufen ist oder nicht.
Lassen Sie uns in AppDelegate.m
einen Standardwert für unser Token registrieren:
+ (void)initialize { NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:@"", @"token", nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; }
Wir fügen dem NetworkManager eine Token-Prüfung hinzu und erhalten Feedback zur Prüfung über Vervollständigungsblöcke. Diese Vervollständigungsblöcke können Sie nach Belieben gestalten. In diesem Beispiel verwende ich „Erfolg“ mit den Antwortobjektdaten und „Fehler“ mit der Fehlerantwortzeichenfolge und einem Statuscode. Hinweis: Ein Fehler kann optional ausgelassen werden, wenn es für die empfangende Seite keine Rolle spielt, z. B. das Erhöhen eines Werts in Analytics.
NetworkManager.h
Über @interface
:
typedef void (^NetworkManagerSuccess)(id responseObject); typedef void (^NetworkManagerFailure)(NSString *failureReason, NSInteger statusCode);
In @interface:
@property (nichtatomar, stark) AFHTTPSessionManager *networkingManager;
- (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;
NetworkManager.m:
Definieren Sie unsere 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]
Wir fügen einige Hilfsmethoden hinzu, um authentifizierte Anfragen zu vereinfachen und Fehler zu analysieren (in diesem Beispiel wird ein JSON-Web-Token verwendet):
- (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"; }
Wenn Sie MBProgressHUD hinzugefügt haben, kann es hier verwendet werden:
#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; } }
Und unsere Token-Check-Anfrage:
- (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); } }]; }
Nun rufen wir in der Methode ViewController.m viewWillAppear diese Singleton-Methode auf. Beachten Sie die Einfachheit der Anforderung und die winzige Implementierung auf der Seite des View Controllers.
[[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]; }];
Das ist es! Beachten Sie, wie dieses Snippet praktisch in jeder Anwendung verwendet werden kann, die die Authentifizierung beim Start überprüfen muss.
Ebenso können wir eine POST-Anfrage zur Anmeldung verarbeiten: 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); } } }
Wir können uns hier etwas einfallen lassen und Warnungen mit AlertController+Blocks im AppDelegate-Fenster hinzufügen oder einfach Fehlerobjekte an den View-Controller zurücksenden. Außerdem könnten wir hier die Anmeldeinformationen des Benutzers speichern oder dies stattdessen dem Ansichtscontroller überlassen. Normalerweise implementiere ich ein separates UserManager-Singleton, das Anmeldeinformationen und Berechtigungen verarbeitet, die direkt mit dem NetworkManager kommunizieren können (persönliche Präferenz).
Noch einmal, die View-Controller-Seite ist super einfach:
- (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 }]; }
Hoppla! Wir haben vergessen, die API zu versionieren und den Gerätetyp zu senden. Außerdem haben wir den Endpunkt von „/checktoken“ auf „/token“ aktualisiert. Da wir unser Netzwerk zentralisiert haben, ist dies super einfach zu aktualisieren. Wir müssen unseren Code nicht durchsuchen. Da wir diese Parameter bei allen Anfragen verwenden, erstellen wir einen Helfer.
#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; }
Zukünftig können beliebig viele gemeinsame Parameter einfach hinzugefügt werden. Dann können wir unsere Token-Prüfung aktualisieren und Methoden wie folgt authentifizieren:
… 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) {
Abschluss unseres AFNetworking-Tutorials
Wir hören hier auf, aber wie Sie sehen können, haben wir allgemeine Netzwerkparameter und -methoden in einem Singleton-Manager zentralisiert, was unsere View-Controller-Implementierungen erheblich vereinfacht hat. Zukünftige Updates werden einfach und schnell sein und, was am wichtigsten ist, es entkoppelt unser Netzwerk von der Benutzererfahrung. Wenn das Designteam das nächste Mal nach einer UI/UX-Überholung fragt, wissen wir, dass unsere Arbeit auf der Netzwerkseite bereits erledigt ist!
In diesem Artikel haben wir uns auf ein Netzwerk-Singleton konzentriert, aber dieselben Prinzipien könnten auf viele andere zentralisierte Funktionen angewendet werden, wie zum Beispiel:
- Umgang mit Benutzerstatus und Berechtigungen
- Routing von Touch-Aktionen zur App-Navigation
- Video- und Audiomanagement
- Analytik
- Benachrichtigungen
- Peripherie
- und sehr viel mehr…
Wir haben uns auch auf eine iOS-Anwendungsarchitektur konzentriert, aber diese könnte genauso einfach auf Android und sogar JavaScript erweitert werden. Als Bonus macht es durch die Erstellung von hochdefiniertem und funktionsorientiertem Code die Portierung von Apps auf neue Plattformen zu einer viel schnelleren Aufgabe.
Zusammenfassend lässt sich sagen, dass Ihr zukünftiger Code sauberer, einfacher und wartungsfreundlicher sein kann, indem Sie ein wenig mehr Zeit in die frühe Projektplanung investieren, um wichtige Singleton-Methoden zu etablieren, wie das obige Netzwerkbeispiel.