[iOS] ド定番OSS!AFNetworking 3.xの使い方

2016.06.11

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

1 はじめに

すいません、いきなり謝ります。ここDevelopers.IOに、「ド定番OSS!AFNetworking 2.xの使い方」という記事がありますが、決して対抗しているつもりはありません、あまりにカッコいいタイトルだったので、パクりました。 ほんとすいません。 m(.)m

AFNetworkingの最新バージョンは、2016年6月現在3.1.0ですが、現状、まだまだ2.xが広く利用されているようです。

バージョン2.xでは、iOS 6をサポートするため、NSURLConnectionを使用したクラスと、iOS7以降で使用可能なNSURLSessionを利用したクラスの2種類が利用可能でしたが、バージョン3.xでは、Xcode7からは非推奨となったNSURLConnectionベースのコードが全て削除され、それに伴い、これを使用したクラス(AFHTTPOperationManager)も利用できなくなっています。

現状、2.xが主流であることから、3.xへの移行要領などは多数紹介されていますが、初めて3.xを導入する方向けの日本語ドキュメントは少ないように感じました。

今回は、対象を3.xに特化してAFNetworkingについて整理してみました。

2 導入

AFNetworkingは、MITライセンスで公開されており、CocoaPodで簡単にインストールが可能です。

pod 'AFNetworking', '~> 3.0'

参考:CocoaPodsによる、外部ライブラリの利用と作成

ライブラリの導入後は、下記のインポートで利用可能になります。

#import "AFNetworking.h"

また、動作に必要な環境は、下記のとおりです。

  • iOS 7以降
  • Mac OS X 10.9以降
  • watchOS 2以降
  • tvOS 9
  • Xcode 7

3 AFURLSessionManagerとAFHTTPSessionManager

AFNetworking 3.xは、NSURLSessionを使用していますが、これにシリアライゼーションやバリデーションなどをラッピングして非常に使いやすくした、AFURLSessionManagerと、さらに、GETやPOST、そしてSuccess(成功),failure(失敗)といったWebアクセスにマッチしたイメージで利用可能な、AFHTTPSessionManagerがあります。

利用場面に応じて、使いやすい方を選択できます。

4 簡単な使用方法

(1) GET

APIサーバからjsonを取得するような処理は、最も利用頻度の高いパターンでしょう。AFHTTPSessionManagerを使用すると、非常にシンプルにこれを記述できます。

ただし、デフォルトで受信シリアライザがjsonとなっているため、レスポンスがjson形式でない場合、このままではエラーとなってしまいます。対応の方法は後述の「5 シリアライザ」を参照して下さい。

NSString *url = @"http://api.example.com/v1/article/123";
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:url parameters:nil progress:nil 
    success:^(NSURLSessionTask *task, id responseObject) {
        // json取得に成功した場合の処理
    } failure:^(NSURLSessionTask *operation, NSError *error) {
        // エラーの場合の処理
    }
];

(2) POST

次の例は、POST処理です。 先のGETの例と比較すると、GETPOSTに、そしてparamatersnilからjson(NSDictionary)に変わっただけです。

SDictionary *json = @{@"age":@25,@"name":@"Taro"};
NSString *url = @"http://api.example.com/v1/CreateArticle";
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager POST:url parameters:json progress:nil   // GETがPOSTに paramatersがnilからjsonに変わった
    success:^(NSURLSessionTask *task, id responseObject) {
        // POSTに成功した場合の処理
    } failure:^(NSURLSessionTask *operation, NSError *error) {
        // エラーの場合の処理
    }
];

この時の送信データは、次のようになっています。

POST /v1/CreateArticle HTTP/1.1
Host: api.example.com
・・・省略・・・
Content-Length: 16

age=25&name=Taro

NSDistionaryのデータが、HTTP本体にKey=value&Key=vlaueの形式で格納されています。

もし、ここで、次のようなjson形式での送信をイメージしていた方は、がっかりでしょう。

POST /v1/CreateArticle HTTP/1.1
Host: api.example.com
・・・省略・・・
Content-Length: 24

{"age":25,"name":"Taro"}

でも、安心してください。これも、GETの時と同じで、デフォルトで送信シリアライザが、HTTP形式になっているからです。

次の1行を追加して送信シリアライザをjsonに変更するだけで、上のようなjson形式で送信されます。

NSDictionary *json = @{@"age":@25,@"name":@"Taro"};
NSString *url = @"http://api.example.com/v1/CreateArticle";
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer = [AFJSONRequestSerializer serializer];
[manager POST:url parameters:json progress:nil   // GETがPOSTに paramatersがnilからjsonに変わった
    success:^(NSURLSessionTask *task, id responseObject) {
        // POSTに成功した場合の処理
    } failure:^(NSURLSessionTask *operation, NSError *error) {
        // エラーの場合の処理
    }
];

(3) POST マルチ-パート

NSString *url = @"http://api.example.com/v1/upload";
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
NSDictionary *parameters = @{@"key": @"value"};
NSURL *filePath = [NSURL fileURLWithPath:@"file://path/to/image.png"];

[manager POST:url parameters:parameters 
     constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        [formData appendPartWithFileURL:filePath name:@"image" error:nil];
     }
     success:^(NSURLSessionTask *task, id responseObject) {
           // POSTに成功した場合の処理
     }
     failure:^(NSURLSessionTask *task, NSError *error) {
           // エラーの場合の処理
     }
];

送信されるデータは、下記のようにマルチパートになっています。

POST /v1/upload HTTP/1.1
Host: api.example.com
Content-Type: multipart/form-data; boundary=Boundary+B10F20C095B45C7C
***省略***
Content-Length: 113

--Boundary+B10F20C095B45C7C
Content-Disposition: form-data; name="key"

value
--Boundary+B10F20C095B45C7C--
*** image.pngのデータ ***

5 シリアライザ

「4 簡単な使用方法」でも、少し触れたとおり、AFNetworkingを使用する場合は、リクエストとレスポンスの形式にシリアライザを合わせることが必要です。ここでシリアライザについて纏めておきます。

(1) 設定

リクエスト(送信)及び、レスポンス(受信)のシリアライザは、それぞれ、次のように設定することができます。

// リクエスト(送信)シリアライザをJSON形式に変更する
manager.requestSerializer = [AFJSONRequestSerializer serializer];
// レスポンス(受信)シリアライザをHTTP形式に変更する
manager.responseSerializer = [AFHTTPResponseSerializer serializer];

(2) 種類

使用できるシリアライザは次のとおりです。

リクエスト(送信)シリアライザ (AFURLRequestSerialization)

  • AFHTTPRequestSerializer HTTP形式(デフォルト)
  • AFJSONRequestSerializer JSON形式
  • AFPropertyListRequestSerializer plist形式

レスポンス(受信)シリアライザ (AFURLResponseSerialization)

  • AFHTTPResponseSerializer HTTP形式
  • AFJSONResponseSerializer JSON形式(デフォルト)
  • AFXMLParserResponseSerializer XML形式
  • AFXMLDocumentResponseSerializer ドキュメント形式(Mac OS Xのみ)
  • AFPropertyListResponseSerializer plist形式
  • AFImageResponseSerializer 画像形式
  • AFCompoundResponseSerializer 上記複合

(3) content-typeの付加

リクエスト(送信)シリアライザの種類に応じて、送信ヘッダに適切なcontent−typeがセットされます。

HTTP形式(AFHTTPRequestSerializer)が設定されている場合の送信ヘッダは、次のようになります。

POST /v1/CreateArticle HTTP/1.1
Host: api.example.com
Content-Type: application/x-www-form-urlencoded
・・・省略・・・
Content-Length: 16

age=25&name=Taro

そして、JSON形式(AFJSONRequestSerializer)を設定した場合のcontent-typeは、application/jsonに変わります。

POST /v1/CreateArticle HTTP/1.1
Host: api.example.com
Content-Type: application/json
・・・省略・・・
Content-Length: 24

{"age":25,"name":"Taro"}

(4) content-typeのバリデーション

content-typeの付加とは逆に、レスポンス(受信)シリアライザの種類によって、受信ヘッダに適切なcontent−typeがセットされているかどうかのバリデーションも行われます。

AFURLResponseSerialization.mを確認すると、対応するcontent-typeは次のようになっています。 (一覧にないものは、バリデーションされていません)

AFJSONResponseSerializer JSON形式

  • application/json
  • text/json
  • text/javascript

AFXMLParserResponseSerializer  XML形式

  • application/xml
  • text/xml

AFXMLDocumentResponseSerializer  ドキュメント形式(Mac OS Xのみ)

  • application/xml
  • text/xml

AFPropertyListResponseSerializer plist形式

  • application/x-plist

AFImageResponseSerializer 画像形式

  • image/tiff
  • image/jpeg
  • image/gif
  • image/png
  • image/ico
  • image/x-icon
  • image/bmp
  • image/x-bmp
  • image/x-xbitmap
  • image/x-win-bitmap

なお、許可するContentTypeは、自分で設定することも可能です。

manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"application/rss+xml"];

6 ヘッダの追加

リクエスト(送信)シリアライザのsetValue:forHTTPHeaderField:メソッドを使用するとヘッダを追加することができます。

下記のように記述すると

[manager.requestSerializer setValue:@"valse" forHTTPHeaderField:@"key"];

送信ヘッダに、次の行が追加されます。

GET /v1/article/123 HTTP/1.1
Host: www.sapporoworks.ne.jp
Accept: */*
key: valse
User-Agent: SampleApp/1.0 (iPhone; iOS 9.3.2; Scale/2.00)
・・・省略・・・

既存のヘッダを上書きすることも可能です。

[manager.requestSerializer setValue:@"valse" forHTTPHeaderField:@"User-Agent"];
GET /v1/article/123 HTTP/1.1
Host: www.sapporoworks.ne.jp
Accept: */*
User-Agent: valse
・・・省略・・・

Cookieは、ヘッダに載せて送信するので、次のようになります。

NSString *cookieString = @"Cookie文字列";
[manager.requestSerializer setValue:cookieString forHTTPHeaderField:@"Cookie"];

7 タイムアウト

デフォルトのタイムアウトは60秒です。タイムアウト値は、リクエスト(送信)シリアライザのsetTimeoutInterval:メソッドで設定できます。単位(秒)

下記の例は、タイムアウトを10秒に設定しています。

[manager.requestSerializer setTimeoutInterval:10];

8 キャッシュ

キャッシュの利用方法は、リクエスト(送信)シリアライザのsetCachePolicy:メソッドで設定できます。

[manager.requestSerializer setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];

設定できるポリシーは次の通りです。

  • NSURLRequestUseProtocolCachePolicy プロトコルのキャッシュポリシーに従う(デフォルト)
  • NSURLRequestReloadIgnoringLocalCacheData キャッシュを利用しない
  • NSURLRequestReloadIgnoringCacheData 上記と同じ
  • NSURLRequestReloadIgnoringLocalAndRemoteCacheData 上記に加え、プロキシーのキャッシュも破棄する
  • NSURLRequestReturnCacheDataElseLoad = キャッシュがあればそれを利用する
  • NSURLRequestReturnCacheDataDontLoad = キャッシュのみを使用する(オフライン動作と同じ)
  • NSURLRequestReloadRevalidatingCacheData = キャッシュの有効性をチェックし、有効な場合のみキャッシュを利用する

9 アップロード・ダウンロード

アップロードやダウンロードは、AFURLSessionManagerを利用すると簡潔に書くことができます。

(1) アップロード

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];

NSURL *url = [NSURL URLWithString:@"http://example.com/upload"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];

NSURL *filePath = [NSURL fileURLWithPath:@"file://path/to/image.png"]; // アップロードするファイル
NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithRequest:request fromFile:filePath progress:nil 
    completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
        if (error) {
            // エラー時の処理
        } else {
            // アップロード成功時の処理
        }
    }
];
[uploadTask resume];

(2) ダウンロード

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];

NSURL *url = [NSURL URLWithString:@"http://example.com/download.zip"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];

NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil 
    destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
        // ここで保存するファイル名を返す
        NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
        return [documentsDirectoryURL URLByAppendingPathComponent:[response suggestedFilename]];
    }
    completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
        if (error) {
            // エラー時の処理
        }else{
            // ダウンロード成功時の処理
        }
    }
];
[downloadTask resume];}

10 経過処理

AFURLSessionManagerでは、通信中の経過処理を書くことができます。

下記の例は、上の「ダウンロード」と全く同じですが、progressブロックでログを出力しています。

・・・省略・・・

NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request 
    progress:^(NSProgress * _Nonnull uploadProgress){
        NSLog(@"loding..."); // 経過中の処理
    }
     destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
        // ここで保存するファイル名を返す
        ・・・省略・・・
    }
     completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
        if (error) {
            // エラー時の処理
        }else{
            // ダウンロード成功時の処理
        }
    }
];
・・・省略・・・

11 ログ

AFNetworkActivityLoggerをインストールすると通信のログをコンソールに出力することができます。

AFNetworking 3.xに対応したAFNetworkActivityLoggerは、2016.06.11現在、github上のbranch '3_0_0'を利用する必要があります。

pod 'AFNetworkActivityLogger', :git => 'https://github.com/AFNetworking/AFNetworkActivityLogger.git', :branch => '3_0_0'

ライブラリの導入後は、下記のインポートで利用可能になります。

#import "AFNetworkActivityLogger.h"

ログ出力を開始するには、下記の行を追加します。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    [[AFNetworkActivityLogger sharedLogger] startLogging];

    return YES;
}

なお、バージョン2.xにあったログのLevelについては、現在、branch 3_0_0 では、実装されていません。

13 最後に

今回は、定番中の定番であるAFNetworkingについて纏めてみました。定番であるがゆえ検索すると多くの情報がヒットしますが、いざ最新のバージョン3.xを使用しようとすると、2.xの多くの情報が雑音になりかねません。 ここで、バージョン3.xに特化したコンテンツに存在価値があればいいのですが・・・

14 参考資料


AFNetworking 3.0 Migration Guide
[Github] AFNetworking
[Cocoapods] AFNetworking 3.1.0
[Github] AFNetworkActivityLogger