iOS 集中和解耦網絡:單例類的 AFNetworking 教程

已發表: 2022-03-11

對於 iOS 架構模式,模型-視圖-控制器 (MVC) 設計模式非常適合應用程序代碼庫的壽命和可維護性。 它允許類通過相互解耦來輕鬆重用或替換以支持各種需求。 這有助於最大限度地發揮面向對象編程 (OOP) 的優勢。

雖然這個 iOS 應用程序架構在微觀層面(應用程序的單個屏幕/部分)運行良好,但隨著應用程序的增長,您可能會發現自己向多個模型添加了類似的功能。 在網絡等情況下,將通用邏輯從模型類中移出並放入單例輔助類中可能是一種更好的方法。 在這個 AFNetworking iOS 教程中,我將教你如何設置一個集中的單例網絡對象,它與微級別的 MVC 組件解耦,可以在整個解耦架構應用程序中重用。

AFNetworking 教程:使用 Singleton 的集中和解耦網絡

iOS網絡的問題

Apple 在易於使用的 iOS SDK 中抽像出管理移動硬件的許多複雜性方面做得很好,但在某些情況下,例如網絡、藍牙、OpenGL 和多媒體處理,類可能會很麻煩,因為它們的目標是保持SDK 靈活。 值得慶幸的是,豐富的 iOS 開發人員社區創建了高級框架來簡化最常見的用例,從而簡化應用程序的設計和結構。 一個優秀的程序員,採用 ios 應用程序架構最佳實踐,知道要使用哪些工具,為什麼要使用它們,以及何時最好從頭開始編寫自己的工具和類。

AFNetworking 是一個很好的網絡示例,也是最常用的開源框架之一,它簡化了開發人員的日常任務。 它簡化了 RESTful API 網絡,並創建了帶有成功、進度和失敗完成塊的模塊化請求/響應模式。 這消除了對開發人員實現的委託方法和自定義請求/連接設置的需求,並且可以非常快速地包含在任何類中。

AFNetworking 的問題

AFNetworking 很棒,但它的模塊化也可能導致它以分散的方式使用。 常見的低效實現可能包括:

  • 在單個視圖控制器中使用類似方法和屬性的多個網絡請求

  • 多個視圖控制器中幾乎相同的請求導致分佈式公共變量可能不同步

  • 一個類中對與該類無關的數據的網絡請求

對於視圖數量有限、要實現的 API 調用很少以及不太可能經常更改的應用程序,這可能不是什麼大問題。 但是,您更有可能想得遠大,併計劃了多年的更新。 如果您的情況是後者,您可能最終需要處理:

  • 支持多代應用程序的 API 版本控制

  • 隨著時間的推移添加新參數或更改現有參數以擴展功能

  • 全新 API 的實現

如果您的網絡代碼分散在您的代碼庫中,那麼這現在是一個潛在的噩夢。 希望您至少在一個公共標頭中靜態定義了一些參數,但即便如此,即使是最細微的更改,您也可能會觸及十幾個類。

我們如何解決 AFNetworking 限制?

創建一個網絡單例以集中處理請求、響應及其參數。

單例對象提供對其類資源的全局訪問點。 單例用於需要這種單點控制的情況,例如提供一些通用服務或資源的類。 您可以通過工廠方法從單例類中獲取全局實例。 - 蘋果

因此,單例是一個類,在應用程序的整個生命週期中,您只會在應用程序中擁有一個實例。 此外,因為我們知道只有一個實例,所以任何其他需要訪問其方法或屬性的類都可以輕鬆訪問它。

這就是我們應該使用單例網絡的原因:

  • 它是靜態初始化的,因此一旦創建,它將具有相同的方法和屬性,任何嘗試訪問它的類都可以使用它。 不會出現奇怪的同步問題或從錯誤的類實例請求數據。

  • 您可以將 API 調用限制在速率限制內(例如,當您必須將 API 請求保持在每秒 5 個以下時)。

  • 主機名、端口號、端點、API 版本、設備類型、持久 ID、屏幕大小等靜態屬性可以放在一起,因此一項更改會影響所有網絡請求。

  • 公共屬性可以在許多網絡請求之間重用。

  • 單例對像在實例化之前不會佔用內存。 這對於具有某些用戶可能永遠不需要的非常特定用例的單身人士很有用,例如在他們沒有設備的情況下將視頻投射到 Chromecast。

  • 網絡請求可以與視圖和控制器完全解耦,因此即使在視圖和控制器被銷毀後它們也可以繼續。

  • 網絡日誌可以集中和簡化。

  • 常見的失敗事件(例如警報)可以重新用於所有請求。

  • 這種單例的主要結構可以通過簡單的頂級靜態屬性更改在多個項目中重用。

不使用單例的一些原因:

  • 它們可以被過度使用以在單個類中提供多種職責。 例如,視頻處理方法可以與網絡方法或用戶狀態方法混合。 這可能是一種糟糕的設計實踐,並導致難以理解的代碼。 相反,應該創建多個具有特定職責的單例。

  • 單例不能被子類化。

  • 單例可以隱藏依賴關係,從而變得不那麼模塊化。 例如,如果一個單例被刪除並且一個類缺少單例導入的導入,它可能會導致未來的問題(特別是如果有外部庫依賴項)。

  • 一個類可以在另一個類中意外的長時間操作期間修改單例中的共享屬性。 如果沒有適當的考慮,結果可能會有所不同。

  • 單例中的內存洩漏可能成為一個重要問題,因為單例本身永遠不會被釋放。

但是,使用 iOS 應用架構最佳實踐,這些負面因素可以得到緩解。 一些最佳實踐包括:

  • 每個單例應該處理一個單一的職責。

  • 如果您需要高精度,請不要使用單例來存儲將被多個類或線程快速更改的數據。

  • 構建單例以根據可用依賴項啟用/禁用功能。

  • 不要在單例屬性中存儲大量數據,因為它們會在應用程序的整個生命週期中持續存在(除非手動管理)。

使用 AFNetworking 的簡單單例示例

首先,作為先決條件,將 AFNetworking 添加到您的項目中。 最簡單的方法是通過 Cocoapods,說明可以在其 GitHub 頁面上找到。

當您使用它時,我建議添加UIAlertController+BlocksMBProgressHUD (再次輕鬆地使用 CocoaPods 添加)。 這些顯然是可選的,但是如果您希望在 AppDelegate 窗口上的單例中實現它們,這將大大簡化進度和警報。

添加AFNetworking ,首先創建一個名為NetworkManager的新 Cocoa Touch 類,作為NSObject的子類。 添加訪問管理器的類方法。 您的NetworkManager.h文件應類似於以下代碼:

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

接下來,實現單例的基本初始化方法並導入 AFNetworking 標頭。 您的類實現應如下所示(注意:假設您使用的是自動引用計數):

 #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

偉大的! 現在我們正在烹飪並準備添加屬性和方法。 作為了解如何訪問單例的快速測試,讓我們將以下內容添加到NetworkManager.h

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

以及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); }

然後在我們的主ViewController.m文件(或任何你有的文件)中,導入NetworkManager.h ,然後在viewDidLoad中添加:

 [[NetworkManager sharedManager] test];

啟動應用程序,您應該在輸出中看到以下內容:

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

好的,所以您可能不會像這樣同時混合#define 、 static const 和@property ,而只是為了清楚起見而顯示您的選項。 “static const” 是一種更好的類型安全聲明,但#define在字符串構建中很有用,因為它允許使用宏。 對於它的價值,我在這種情況下使用#define來簡潔。 除非您使用指針,否則這些聲明方法之間在實踐中沒有太大區別。

現在您已經了解了#defines 、常量、屬性和方法,我們可以刪除這些並轉到更相關的示例。

網絡示例

想像一個應用程序,用戶必須登錄才能訪問任何內容。 在應用程序啟動時,我們將檢查是否保存了身份驗證令牌,如果是,則對我們的 API 執行 GET 請求以查看令牌是否已過期。

AppDelegate.m中,讓我們為我們的令牌註冊一個默認值:

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

我們將向 NetworkManager 添加令牌檢查,並通過完成塊獲得有關檢查的反饋。 您可以隨心所欲地設計這些完成塊。 在此示例中,我將成功與響應對像數據一起使用,並將失敗與錯誤響應字符串和狀態代碼一起使用。 注意:如果失敗對接收方無關緊要,例如在分析中增加值,則可以選擇忽略失敗。

網絡管理器.h

@interface上方:

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

在@界面中:

@property (nonatomic, strong) AFHTTPSessionManager *networkingManager;

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

網絡管理器.m:

定義我們的 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]

我們將添加一些幫助方法來簡化經過身份驗證的請求以及解析錯誤(此示例使用 JSON Web 令牌):

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

如果你添加了 MBProgressHUD,它可以在這裡使用:

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

還有我們的令牌檢查請求:

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

現在,在 ViewController.m viewWillAppear 方法中,我們將調用這個單例方法。 請注意請求的簡單性和 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]; }];

而已! 請注意,此代碼段實際上是如何在任何需要在啟動時檢查身份驗證的應用程序中使用的。

同樣,我們可以處理登錄的 POST 請求:NetworkManager.h:

 - (void)authenticateWithEmail:(NSString*)email password:(NSString*)password success:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;

網絡管理器.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); } } }

我們可以在這里花哨並在 AppDelegate 窗口上使用 AlertController+Blocks 添加警報,或者簡單地將失敗對象發送回視圖控制器。 此外,我們可以在這裡保存用戶憑據,或者讓視圖控制器處理它。 通常,我實現一個單獨的 UserManager 單例來處理可以直接與 NetworkManager 通信的憑據和權限(個人偏好)。

再一次,視圖控制器端非常簡單:

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

哎呀! 我們忘記對 API 進行版本化並發送設備類型。 此外,我們已將端點從“/checktoken”更新為“/token”。 由於我們集中了我們的網絡,這非常容易更新。 我們不需要深入研究我們的代碼。 由於我們將在所有請求上使用這些參數,因此我們將創建一個幫助程序。

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

將來可以輕鬆地將任何數量的通用參數添加到其中。 然後我們可以更新我們的令牌檢查和身份驗證方法,如下所示:

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

結束我們的 AFNetworking 教程

我們將在此停止,但如您所見,我們在單例管理器中集中了常見的網絡參數和方法,這大大簡化了我們的視圖控制器實現。 未來的更新將簡單快速,最重要的是,它將我們的網絡與用戶體驗分離。 下次設計團隊要求對 UI/UX 進行大修時,我們會知道我們的工作已經在網絡方面完成了!

在本文中,我們專注於網絡單例,但這些相同的原則可以應用於許多其他集中式功能,例如:

  • 處理用戶狀態和權限
  • 將觸摸操作路由到應用導航
  • 視頻和音頻管理
  • 分析
  • 通知
  • 外圍設備
  • 還有更多……

我們還專注於 iOS 應用程序架構,但這可以很容易地擴展到 Android 甚至 JavaScript。 作為獎勵,通過創建高度定義和麵向函數的代碼,它使將應用程序移植到新平台成為一項更快的任務。

總而言之,通過在早期項目計劃中花費一些額外的時間來建立關鍵的單例方法,就像上面的網絡示例一樣,您未來的代碼可以更簡潔、更易於維護。