iOSの集中型および分離型ネットワーキング:シングルトンクラスを使用したAFNetworkingチュートリアル
公開: 2022-03-11iOSアーキテクチャパターンに関して言えば、Model-View-Controller(MVC)デザインパターンは、アプリケーションのコードベースの寿命と保守性に優れています。 クラスを簡単に再利用または置換して、クラスを相互に分離することにより、さまざまな要件をサポートできます。 これは、オブジェクト指向プログラミング(OOP)の利点を最大化するのに役立ちます。
このiOSアプリケーションアーキテクチャはマイクロレベル(アプリの個々の画面/セクション)でうまく機能しますが、アプリが成長するにつれて、複数のモデルに同様の機能を追加することに気付くかもしれません。 ネットワーキングなどの場合、共通ロジックをモデルクラスからシングルトンヘルパークラスに移動することをお勧めします。 このAFNetworkingiOSチュートリアルでは、マイクロレベルのMVCコンポーネントから分離され、分離されたアーキテクチャアプリケーション全体で再利用できる集中型シングルトンネットワークオブジェクトをセットアップする方法を説明します。
iOSネットワークの問題
Appleは、使いやすいiOS SDKでモバイルハードウェアを管理する際の複雑さの多くを抽象化するという素晴らしい仕事をしてきましたが、ネットワーキング、Bluetooth、OpenGL、マルチメディア処理などの場合、クラスを維持することを目的としているため、クラスが煩雑になることがあります。 SDKは柔軟です。 ありがたいことに、豊富なiOS開発者コミュニティは、アプリケーションの設計と構造を簡素化するために、最も一般的なユースケースを簡素化するための高レベルのフレームワークを作成しました。 優れたプログラマーは、iOSアプリアーキテクチャのベストプラクティスを採用しており、使用するツール、それらを使用する理由、および独自のツールとクラスを最初から作成する方がよい場合を知っています。
AFNetworkingは優れたネットワーキングの例であり、最も一般的に使用されているオープンソースフレームワークの1つであり、開発者の日常のタスクを簡素化します。 これにより、RESTful APIネットワーキングが簡素化され、成功、進行、および失敗の完了ブロックを含むモジュール式の要求/応答パターンが作成されます。 これにより、開発者が実装したデリゲートメソッドやカスタムのリクエスト/接続設定が不要になり、どのクラスにもすばやく含めることができます。
AFNetworkingの問題
AFNetworkingは素晴らしいですが、そのモジュール性は断片化された方法での使用にもつながる可能性があります。 一般的な非効率的な実装には、次のものが含まれます。
単一のViewControllerで同様のメソッドとプロパティを使用する複数のネットワークリクエスト
同期が外れる可能性のある分散共通変数につながる、複数のViewControllerでのほぼ同一のリクエスト
そのクラスに関係のないデータに対するクラス内のネットワーク要求
ビューの数が限られていて、実装するAPI呼び出しが少なく、頻繁に変更される可能性が低いアプリケーションの場合、これは大きな問題にはならない可能性があります。 ただし、考えている可能性が高く、何年にもわたる更新が計画されています。 後者の場合は、次の処理が必要になる可能性があります。
複数世代のアプリケーションをサポートするためのAPIバージョニング
機能を拡張するための新しいパラメータの追加または既存のパラメータへの変更
まったく新しいAPIの実装
ネットワークコードがコードベース全体に散在している場合、これは潜在的な悪夢になります。 うまくいけば、共通のヘッダーで静的に定義されたパラメーターの少なくとも一部がありますが、それでも、ごくわずかな変更でさえ、12のクラスに触れる可能性があります。
AFNetworkingの制限にどのように対処しますか?
ネットワークシングルトンを作成して、要求、応答、およびそれらのパラメーターの処理を一元化します。
シングルトンオブジェクトは、そのクラスのリソースへのグローバルなアクセスポイントを提供します。 シングルトンは、一般的なサービスやリソースを提供するクラスなど、この単一の制御ポイントが望ましい状況で使用されます。 ファクトリメソッドを介してシングルトンクラスからグローバルインスタンスを取得します。 - りんご
したがって、シングルトンは、アプリケーションの存続期間中に存在するアプリケーション内に1つのインスタンスしか存在しないクラスです。 さらに、インスタンスは1つしかないことがわかっているため、メソッドやプロパティにアクセスする必要のある他のクラスから簡単にアクセスできます。
ネットワーキングにシングルトンを使用する必要がある理由は次のとおりです。
静的に初期化されるため、作成されると、アクセスしようとするすべてのクラスで使用できるのと同じメソッドとプロパティがあります。 奇妙な同期の問題が発生したり、クラスの間違ったインスタンスからデータを要求したりする可能性はありません。
API呼び出しを制限して、レート制限を下回るようにすることができます(たとえば、APIリクエストを1秒あたり5未満に保つ必要がある場合)。
ホスト名、ポート番号、エンドポイント、APIバージョン、デバイスタイプ、永続ID、画面サイズなどの静的プロパティを同じ場所に配置できるため、1つの変更がすべてのネットワーク要求に影響します。
共通のプロパティは、多くのネットワーク要求間で再利用できます。
シングルトンオブジェクトは、インスタンス化されるまでメモリを消費しません。 これは、デバイスを持っていない場合にChromecastへの動画のキャストを処理するなど、一部のユーザーが必要としない可能性のある非常に特殊なユースケースを持つシングルトンに役立ちます。
ネットワーク要求は、ビューとコントローラーから完全に切り離すことができるため、ビューとコントローラーが破棄された後でも続行できます。
ネットワークロギングは一元化および簡素化できます。
アラートなどの障害の一般的なイベントは、すべての要求に再利用できます。
このようなシングルトンの主な構造は、単純なトップレベルの静的プロパティの変更により、複数のプロジェクトで再利用できます。
シングルトンを使用しないいくつかの理由:
それらは、単一のクラスで複数の責任を提供するために乱用される可能性があります。 たとえば、ビデオ処理方法は、ネットワーク方法またはユーザー状態方法と混合することができます。 これはおそらく不十分な設計手法であり、コードを理解するのが困難になります。 代わりに、特定の責任を持つ複数のシングルトンを作成する必要があります。
シングルトンはサブクラス化できません。
シングルトンは依存関係を隠すことができるため、モジュール性が低くなります。 たとえば、シングルトンが削除され、シングルトンがインポートしたインポートがクラスにない場合、将来の問題が発生する可能性があります(特に、外部ライブラリの依存関係がある場合)。
クラスは、別のクラスでは予期しない長い操作中に、シングルトンで共有プロパティを変更できます。 これを適切に考慮しないと、結果が異なる場合があります。
シングルトン自体が割り当て解除されることはないため、シングルトンでのメモリリークは重大な問題になる可能性があります。
ただし、iOSアプリアーキテクチャのベストプラクティスを使用すると、これらの欠点を軽減できます。 いくつかのベストプラクティスは次のとおりです。
各シングルトンは、単一の責任を処理する必要があります。
高精度が必要な場合は、複数のクラスまたはスレッドによって急速に変更されるデータを格納するためにシングルトンを使用しないでください。
シングルトンを構築して、利用可能な依存関係に基づいて機能を有効/無効にします。
大量のデータをシングルトンプロパティに保存しないでください。データはアプリケーションの存続期間中存続します(手動で管理されている場合を除く)。
AFNetworkingを使用した単純なシングルトンの例
まず、前提条件として、プロジェクトにAFNetworkingを追加します。 最も簡単なアプローチはCocoapodsを使用する方法で、手順はGitHubページにあります。
その間、 UIAlertController+Blocks
とMBProgressHUD
を追加することをお勧めします(これもCocoaPodsで簡単に追加できます)。 これらは明らかにオプションですが、AppDelegateウィンドウのシングルトンで実装したい場合は、進行状況とアラートが大幅に簡素化されます。
AFNetworking
が追加されたら、 NSObject
のサブクラスとしてNetworkManager
と呼ばれる新しいCocoaTouchクラスを作成することから始めます。 マネージャにアクセスするためのクラスメソッドを追加します。 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
を一度に混在させるのではなく、オプションを明確にするために表示するだけです。 「staticconst」は型安全性の宣言として優れていますが、 #define
はマクロを使用できるため、文字列の作成に役立ちます。 このシナリオでは、簡潔にするために#define
を使用しています。 ポインタを使用していない限り、これらの宣言アプローチの間に実際の違いはあまりありません。
#defines
、定数、プロパティ、およびメソッドを理解したので、それらを削除して、より関連性の高い例に進むことができます。
ネットワーキングの例
ユーザーが何かにアクセスするにはログインする必要があるアプリケーションを想像してみてください。 アプリの起動時に、認証トークンを保存したかどうかを確認し、保存した場合は、APIに対してGETリクエストを実行して、トークンの有効期限が切れているかどうかを確認します。
AppDelegate.m
で、トークンのデフォルトを登録しましょう。
+ (void)initialize { NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:@"", @"token", nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; }
NetworkManagerにトークンチェックを追加し、完了ブロックを介してチェックに関するフィードバックを取得します。 これらの完了ブロックは、好きなように設計できます。 この例では、応答オブジェクトデータで成功を使用し、エラー応答文字列とステータスコードで失敗を使用しています。 注:分析で値をインクリメントするなど、受信側にとって問題がない場合は、オプションで失敗を除外できます。
NetworkManager.h
@interface
の上:
typedef void (^NetworkManagerSuccess)(id responseObject); typedef void (^NetworkManagerFailure)(NSString *failureReason, NSInteger statusCode);
@interface:
@property(nonatomic、strong)AFHTTPSessionManager * networkingManager;
- (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;
NetworkManager.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メソッドで、このシングルトンメソッドを呼び出します。 リクエストの単純さと、ViewController側の小さな実装に注目してください。
[[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;
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); } } }
ここで気を付けて、AppDelegateウィンドウのAlertController + Blocksを使用してアラートを追加するか、単に障害オブジェクトをViewControllerに送り返すことができます。 さらに、ここにユーザークレデンシャルを保存するか、代わりにViewControllerに処理させることができます。 通常、NetworkManagerと直接通信できる資格情報とアクセス許可を処理する個別のUserManagerシングルトンを実装します(個人設定)。
繰り返しになりますが、ViewController側は非常にシンプルです。
- (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にも簡単に拡張できます。 ボーナスとして、高度に定義された機能指向のコードを作成することで、アプリを新しいプラットフォームに移植する作業がはるかに迅速になります。
要約すると、上記のネットワークの例のように、プロジェクトの初期計画に少し余分な時間を費やして主要なシングルトンメソッドを確立することで、将来のコードをよりクリーンでシンプルに、より保守しやすくすることができます。