話題の記事

ド定番OSS!AFNetworking 2.xの使い方

2014.02.04

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

今更なんだよ?って気がしますが、うちのブログにAFNetworkingについての記事が無いので軽く書いてみます。

2.x系になって変わったこと

まず、一番の変更点はAFHTTPClientがいなくなったことでしょうか。変わりにAFHTTPOperationManagerAFHTTPSessionManagerなるものや、AFXxxRequestSerializerAFXxxResponseSerializerなどが追加になりました。また、動作可能なiOSのバージョンは6.0以降になってました。

なんだこれ?ってわけで早速触ってみます。

AFXxxManager

AFHTTPOperationManagerAFHTTPSessionManagerがありますが、どうやらiOS 6.xに対応するのであればAFHTTPOperationManagerを、iOS 7.x以降であればAFHTTPSessionManagerを使うといいらしいです。もう既にお分かりかと思いますが、AFHTTPSessionManagerはiOS 7で追加されたNSURLSessionを使っているのでそれもそのはずです。

なので案件によって使い分ける必要です。受託開発の場合はまだiOS 6.xは切れなそうなので、AFHTTPOperationManagerを使うことが多くなりそうです。

2.x系からはこのAFXxxManagerを使用して通信処理を記述します。例えば、単にJSON データを取得するのであれば以下のように書きます。

AFHTTPOperationManagerの場合

// AFHTTPRequestOperationManagerを利用して、http://localhost/test.jsonからJSONデータを取得する
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

[manager GET:@"http://localhost/test.json"
  parameters:nil
     success:^(AFHTTPRequestOperation *operation, id responseObject) {
         // 通信に成功した場合の処理
         NSLog(@"responseObject: %@", responseObject);
     }
     failure:^(AFHTTPRequestOperation *operation, NSError *error) {
         // エラーの場合はエラーの内容をコンソールに出力する
         NSLog(@"Error: %@", error);
     }];

なんかAFHTTPClientのときとあんまり変わらない気もしますが、よりシンプルになった気がします。

GET〜のようにHTTPメソッドごとにPOST〜、PUT〜が定義されてます。あと、これらのメソッドは実行と同時に通信処理が開始されるので、戻り値のインスタンスで- startメソッドとか呼ぶ必要はありません。ここではコンビニエンスコンストラクタを使ってAFHTTPOperationManagerのインスタンスを生成しましたが、- initWithBaseURL:でベースURLを指定することもできます。その場合、これらのHTTPメソッドごとのメソッド内ではURLStringにはベースURL以下のパスを記述すれば良さげです。

ただし、- HTTPRequestOperationWithRequest:success:failure:の場合は自分でNSURLRequestインスタンスを生成するのでベースURLは関係ないようです。また、このメソッド使う場合はHTTPメソッド系のメソッド(ややこしい・・)のようにメソッド実行時に通信処理は開始されないので、明示的に- startメソッドを呼ばなければなりません。これはAFHTTPSessionManagerも一緒のようです。

AFHTTPSessionManagerの場合

ちなみに、AFHTTPSessionManagerではこう書きます。

// AFHTTPSessionManagerを利用して、http://localhost/test.jsonからJSONデータを取得する
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

[manager GET:@"http://localhost/test.json"
  parameters:nil
     success:^(NSURLSessionDataTask *task, id responseObject) {
         // 通信に成功した場合の処理
         NSLog(@"responseObject: %@", responseObject);
     } failure:^(NSURLSessionDataTask *task, NSError *error) {
         // エラーの場合はエラーの内容をコンソールに出力する
         NSLog(@"Error: %@", error);
     }];

AFHTTPOperationManagerとの違いは、戻り値とBlocksの第1引数がAFHTTPRequestOperationではなく、NSURLSessionDataTaskのインスタンスになるんですね。

AFXxxRequestSerializerとAFXxxResponseSerializer

個人的に、この変更が一番キターーーーーってなりました。受託開発の場合、リクエストやレスポンスをごにょごにょすることが非常に多いので、これらを別クラスとして再設計してもらえるのは非常にありがたいです。 このAFXxxRequestSerializerAFXxxResponseSerializerは文字通りリクエストとレスポンスをシリアライズしてくれるクラスです。当然、それぞれ個別に指定できます。例えばリクエストパラメータはJSONで、レスポンスはplistでってことも簡単です。

今までAFHTTPClientのサブクラスを作成してAPIの仕様に合わせた実装を書いていた場合、この辺が代用できそうです。

AFXxxRequestSerializer

AFXxxRequestSerializerは、NSMutableURLRequestを生成するためのクラスです。リクエストパラメータの形式ごとに以下の3つが用意されています。

AFHTTPRequestSerializer
HTTPリクエストパラメータ(デフォルト)
AFJSONRequestSerializer
JSON形式のリクエストパラメータ
AFPropertyListRequestSerializer
plist形式のリクエストパラメータ

どのAFXxxRequestSerializerを使用するかを指定するには以下のように書きます。

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

// JSON形式のリクエストパラメータを送信する
manager.requestSerializer = [AFJSONRequestSerializer serializer];

具体的には、AFHTTPRequestSerializerは、例えばGETメソッドであれば、http://localhost/?hoge=fugaのようにURLストリングにパラメータを設定し、POSTであればHTTPリクエストのボディにパラメータをセットします。

AFJSONRequestSerializerの場合は、HTTPリクエストのボディにJSON形式にシリアライズしたデータをセットします。同様に、AFPropertyListRequestSerializerの場合はHTTPリクエストのボディにplist形式にシリアライズしたデータをセットします。

このとき、送信するパラメータの形式に合わせて、HTTPリクエストヘッダにcontent-typeの設定も行ってくれます。

HTTPリクエストヘッダ

AFXxxRequestSerializerではリクエストヘッダに独自のパラメータを設定することもできます。

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

// HTTPリクエストヘッダにアプリバージョンをセットする
[manager.requestSerializer setValue:@"1.0.0" forHTTPHeaderField:@"app-version"];

タイムアウトとキャッシュポリー

タイムアウトやキャシュポリーはそれぞれNSURLRequestのtimeoutIntervalプロパティ、cachePolicyプロパティに設定します。この場合、AFXxxRequestSerializerのサブクラスを作成し、NSURLRequestのインスタンスを生成する唯一のメソッドである- requestWithMethod:URLString:parameters:をオーバーライドするのが手っ取り早そうです。

@implementation MyHTTPRequestSerializer

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                 URLString:(NSString *)URLString
                                parameters:(NSDictionary *)parameters
{
    NSMutableURLRequest *request = [super requestWithMethod:method
                                                  URLString:URLString
                                                 parameters:parameters];

    // タイムアウトとキャッシュポリシーを設定する
    request.timeoutInterval = kTimeoutInterval;
    request.cachePolicy     = NSURLRequestReloadIgnoringCacheData;

    return request;
}

@end

AFXxxResponseSerializer

AFXxxResponseSerializerでは

AFHTTPResponseSerializer
HTTPレスポンス
AFJSONResponseSerializer
JSON形式のレスポンス(デフォルト)
AFXMLParserResponseSerializer
XML形式のレスポンス
AFPropertyListResponseSerializer
plist形式のレスポンス
AFImageResponseSerializer
画像
AFCompoundResponseSerializer
上記のResponseSerializerの複合

が用意されています。どのAFXxxResponseSerializerを使用するかを指定するには以下のように書きます。

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

// plist形式のリクエストパラメータを送信する
manager.responseSerializer = [AFPropertyListResponseSerializer serializer];

AFXMLDocumentResponseSerializerは残念ながらOS Xのみです。 AFXxxResponseSerializerはレスポンスのシリアライズだけでなく、content-typeステータスコードによるバリデーションも行っています。例えば、AFXxxManagerresponseSerializerプロパティがAFJSONResponseSerializerなのに、実際のレスポンスのcontent-typeがtext/htmlだったりするとエラー扱いとなります。それぞれ対応するcontent-typeは以下の通りです。

AFHTTPResponseSerializer
指定なし
AFJSONResponseSerializer
application/json、text/json、text/javascript
AFXMLParserResponseSerializer
application/xml、text/xml
AFPropertyListResponseSerializer
application/x-plist
AFImageResponseSerializer
image/*(jpeg、png、gifなど)

AFXxxManagerのresponseSerializerプロパティにはデフォルトでAFJSONResponseSerializerがセットされており、確認用に適当なURLを読み込んでみようとすると「レスポンスがJSONじゃないよー」って言われちゃうので、注意してください。

AFCompoundResponseSerializerは少し特殊で、複数のAFXxxResponseSerializerをまとめて設定することが可能です。この場合、AFCompoundResponseSerializerのコンビニエンスコンストラクタである+ compoundSerializerWithResponseSerializers:の引数に、使用するAFXxxResponseSerializerインスタンスの配列を渡す必要があります。

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

// AFJSONResponseSerializer、AFHTTPResponseSerializerの順にレスポンスを解析
NSArray *responseSerializers =
@[
  [AFJSONResponseSerializer serializer],
  [AFHTTPResponseSerializer serializer]
  ];

AFCompoundResponseSerializer *responseSerializer = [AFCompoundResponseSerializer compoundSerializerWithResponseSerializers:responseSerializers];

manager.responseSerializer = responseSerializer;

[manager GET:@"http://localhost/test.json"
  parameters:nil
     success:^(AFHTTPRequestOperation *operation, id responseObject) {
         // 通信に成功した場合の処理
         NSLog(@"responseObject: %@", [responseObject class]);
     }
     failure:^(AFHTTPRequestOperation *operation, NSError *error) {
         // エラーの場合はエラーの内容をコンソールに出力する
         NSLog(@"Error: %@", error);
     }];

どのAFXxxResponseSerializerが適用されるかは、配列を順に走査して適用できるAFXxxResponseSerializerがあればシリアライズを実行して終了します。ですので指定する順序に注意が必要です。例えば、

NSArray *responseSerializers =
@[
  [AFHTTPResponseSerializer serializer],
  [AFJSONResponseSerializer serializer]
  ];

とすると、JSON形式のレスポンスでもAFHTTPResponseSerializerがシリアライズを実行してしまい、responsObjectはNSDataとなってしまいます。

独自のリザルトコードに対応する

独自のリザルトコードに対応する場合は、AFXxxResponseSerializerのサブクラスを作成すると綺麗に書けるかもしれません。

@implementation MyJSONResponseSerializer

- (id)responseObjectForResponse:(NSURLResponse *)response
                           data:(NSData *)data
                          error:(NSError *__autoreleasing *)error
{
    id responseObject = [super responseObjectForResponse:response data:data error:error];

    if (error && *error) {
        return nil;
    }

    NSDictionary *json = (NSDictionary *)responseObject;

    NSNumber *myResultCode = json[@"resultCode"];

    if (myResultCode.integerValue == 0) {
        // 独自API成功
        return responseObject;
    } else {
        // 独自APIエラー
        NSString *errorMessage = json[@"errorMessage"];

        NSDictionary *userInfo = @{
                                   NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"My API is error!: %@ (%d)", @"MyProject", nil), errorMessage, myResultCode],
                                   NSURLErrorFailingURLErrorKey: [response URL],
                                   AFNetworkingOperationFailingURLResponseErrorKey: response
                                   };
        if (error) {
            *error = [[NSError alloc] initWithDomain:MyProjectErrorDomain code:NSURLErrorBadServerResponse userInfo:userInfo];
        }

        return nil;
    }
}

@end

ファイルのダウンロード・アップロード(AFHTTPOperationManagerの場合)

AFHTTPOperationManagerを使用する場合、ファイルのダウンロード・アップロードの処理は以下のように記述します。AFHTTPOperationManagerの方はたぶんAFNetworking 1.3系とそこまで変わらない気がしてます。

ファイルのダウンロード

>// 「http://localhost/test.zip」をダウンロードする
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

// ダウンロード先のURLを設定したNSURLRequestインスタンスを生成する
NSMutableURLRequest *request = [manager.requestSerializer requestWithMethod:@"GET"
                                                                  URLString:@"http://localhost/test.zip"
                                                                 parameters:nil];

// ダウンロード処理を実行するためのAFHTTPRequestOperationインスタンスを生成する
AFHTTPRequestOperation *operation = [manager HTTPRequestOperationWithRequest:request
                                                                     success:^(AFHTTPRequestOperation *operation, id responseObject) {
                                                                         // ダウンロードに成功したらコンソールに成功した旨を表示する
                                                                         NSLog(@"ダウンロード完了!");
                                                                     } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                                                                         // エラーの場合はエラーの内容をコンソールに出力する
                                                                         NSLog(@"Error: %@", error);
                                                                     }];


// データを受信する度に実行される処理を設定する
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
    // ダウンロード中の進捗状況をコンソールに表示する
    NSLog(@"bytesRead: %@, totalBytesRead: %@, totalBytesExpectedToRead: %@, progress: %@",
          @(bytesRead),
          @(totalBytesRead),
          @(totalBytesRead),
          @((float)totalBytesRead / totalBytesExpectedToRead));
}];

// <Application_Home>/Documentsディレクトリのパスを取得する
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = paths[0];

// <Application_Home>/Documents/test.zip
NSString *filePath = [documentDirectory stringByAppendingPathComponent:@"test.zip"];

NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:filePath]) {
    [fileManager removeItemAtPath:filePath error:nil];
}

// ファイルの保存先を「<Application_Home>/Documents/test.zip」に指定する
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:filePath append:NO];

// ダウンロードを開始する
[manager.operationQueue addOperation:operation];

ファイルのアップロード

// <Application_Home>/Documentsディレクトリのパスを取得する
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = paths[0];

// <Application_Home>/Documents/test.png
NSString *imageFilePath = [documentDirectory stringByAppendingPathComponent:@"test.jpg"];

// 画像ファイルからNSDataインスタンスを生成する
NSData *imageData = [NSData dataWithContentsOfFile:imageFilePath];
NSLog(@"imageData : %@", imageFilePath);

// 「http://localhost/uploadfile.php」に<Application_Home>/Documents/test.pngをアップロードする
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer new];

// アップロード先のURLを設定したNSURLRequestインスタンスを生成する
NSMutableURLRequest *request = [manager.requestSerializer multipartFormRequestWithMethod:@"POST"
                                                                               URLString:@"http://localhost/uploadfile.php"
                                                                              parameters:@{
                                                                                           @"param1": @"その他のパラメータ1",
                                                                                           @"param2": @"その他のパラメータ2"
                                                                                           }
                                                               constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
                                                                   [formData appendPartWithFileData:imageData
                                                                                               name:@"image1"
                                                                                           fileName:@"test.jpg"
                                                                                           mimeType:@"image/jpeg"];
                                                               }
                                                                                   error:NULL];

// アップロード処理を実行するためのAFHTTPRequestOperationインスタンスを生成する
AFHTTPRequestOperation *operation = [manager HTTPRequestOperationWithRequest:request
                                                                     success:^(AFHTTPRequestOperation *operation, id responseObject) {
                                                                         // アップロードに成S功したらコンソールに成功した旨を表示する
                                                                         // NSData型のresponseObjectをNSStringに変換する
                                                                         NSString *responseStr = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];

                                                                         // 取得したレスポンスデータをコンソールに出力する
                                                                         NSLog(@"responseStr: %@", responseStr);
                                                                     } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                                                                         // エラーの場合はエラーの内容をコンソールに出力する
                                                                         NSLog(@"Error: %@", error);
                                                                     }];

// データを送信する度に実行される処理を設定する
[operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
    // アップロード中の進捗状況をコンソールに表示する
    NSLog(@"bytesWritten: %@, totalBytesWritten: %@, totalBytesExpectedToWrite: %@, progress: %@",
          @(bytesWritten),
          @(totalBytesWritten),
          @(totalBytesExpectedToWrite),
          @((float)totalBytesWritten / totalBytesExpectedToWrite));
}];

// アップロードを開始する
[manager.operationQueue addOperation:operation];

AFHTTPSessionManagerを使用する場合

AFHTTPSessionManagerの場合は、AFHTTPOperationManagerの時と比べ少し扱いが異なます。というのも、AFHTTPSessionManagerではiOS 7から追加されたNSURLSessionDownloadTaskやらNSURLSessionUploadTaskやらを使います(ここに関しては後で詳しく書こう思います)。

リトライ処理

AFNetworkingでは、標準では自動リトライ処理に対応していません。なので、自前で実装する必要があります。自動リトライ処理を実装する方法としては、AFXxxManangerのサブクラスやカテゴリーを実装する方法があります。特にカテゴリーで実装する方法はshaioz/AFNetworking-AutoRetryに非常に良いサンプルがあるので、必要な方は参考にしてみるといいと思います。

まとめ

ざっくりとはこんな感じでしょうか。あとは、AFNetworkActivityLoggerを使ってログを出力したり、セキュリティポリシーの設定をいじってオレオレ証明書を使ってSSL通信通信したり(この辺の内容はiOS - AFNetworking 2.0 のまとめ - Qiita [キータ] にわかりやすく書いてありました!)なども簡単に行えるようになったようです。

1.3系と比べ、かなり使いやすくなった印象ですね。ほんと素晴らしいライブラリです!