[iOS]AFHTTPSessionManagerのエラー判別時に使えるテクニック

2014.07.19

ことはじめ

こんばんは!

最近、未知のunsigned charたちと戯れていたあらかわです。

今回はiOSアプリ開発において、HTTP通信ライブラリのデファクトスタンダードとなっているAFNetworkingAFHTTPSessionManagerについて書いていきます。

※基本的な事は弊社平井の記事「ド定番OSS!AFNetworking 2.xの使い方」で解説してありますのでそちらをご覧下さい。

CocoaPods

CocoaPodsの導入から解説していますので、既にご存知の方は次の見出しまで飛ばして下さい。

まだ使ったことのない方は是非この機会に使用してみましょう!

AFNetworkingはCocoaPodsでインストールすると良いです。

CocoaPodsがまだ入っていないという方はターミナルにて以下のコマンドを入力して下さい。

$ sudo gem install cocoapods

パスワードを聞かれたら入力して、完了後

$ pod setup

以上で完了です。CocoaPodsはGitHubなどからライブラリをインストールしてくれる便利なツールです。

コンフリクトや、ソースコード納品時に動かなくなるなどを避けたい方はCocoaPodsを絶対に入れましょう!

CocoaPodsを使用するメリット

  • 複数人でプロジェクトを管理する時に、個々がダウンロードしてきたライブラリのバージョン違いなどが起きません。
  • それぞれのライブラリの依存関係までチェックしてくれます。(例, AとBのライブラリを同時に使ったらバグが起きたよ!という現象を回避)
  • 使用するライブラリの好みのバージョンを指定してダウンロードできます。最新版が不安定、もしくは既存バージョンと大幅な変更の時などは大いに助かります。
  • JenkinsなどのCIツールを使っていても簡単なシェル設定をすれば、ビルド時にライブラリをインストールできます。

AFNetworkingをプロジェクトに導入

Xcodeでプロジェクトを作成後、ターミナルで対象のプロジェクトディレクトリ直下に移動し、pod initコマンドを叩きます。

~/Develop/iOS/SampleAFHTTPSessionManager $ pod init

そうするとPodfileなるものが同ディレクトリに出来ています。

vim Podfile

Podfileを編集します。テキストエディタやXcodeで編集しても構いません。

# Uncomment this line to define a global platform for your project
platform :ios, "7.0"

target "SampleAFHTTPSessionManager" do

pod 'AFNetworking', '~> 2.3.1'

end

target "SampleAFHTTPSessionManagerTests" do

end

ターゲットによって導入するライブラリを変えられます。iOSのバージョンはアプリ毎の要件などによって変えましょう。

これでAFNetworkingのバージョン2.3.1をインストールする準備が整いました。バージョンを指定しなければ最新バージョンが取れます。

Podfileとプロジェクトファイルが存在するディレクトリで以下のコマンドを打てばインストールされます。

インストールする前にはXcodeを終了させておくと良いです。

$ pod install
Analyzing dependencies
Downloading dependencies
Installing AFNetworking (2.3.1)
Generating Pods project
Integrating client project

同ディレクトリ内に.xcworkspaceファイルが出来ていますので開きます。以降はプロジェクトファイルではなく、ワークスペースファイルでプロジェクトを編集していきます。

$ open ./SampleAFHTTPSessionManager.xcworkspace/

それでは本題のAFHTTPSessionManagerについて解説して行きます。

AFHTTPSessionManagerでエラー時のレスポンスオブジェクトの取得の方法

iOS7から追加されたNSURLSessionを簡単に使えるクラス、AFHTTPSessionManagerでレスポンスオブジェクトを取得してみましょう。

AFHTTPOperationManagerと違うところは、バックグラウンドでデータのダウンロードなどが行えるところです。

AFHTTPSessionManagerの実装例

通信を行いたい箇所で以下のように実装します。

// リクエスト時に送信するパラメータです。
NSDictionary *param = @{
                        @"user_id" : @1,
                        @"password" : @"cocoapods"
                        };

// HTTP通信モジュールです。
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

// リクエストはJSON形式で送信します。
manager.requestSerializer = [AFJSONRequestSerializer serializer];
// レスポンスもJSON形式で受け取ります。
manager.responseSerializer = [AFJSONResponseSerializer serializer];
// タイムアウト時間を設定します。
manager.requestSerializer.timeoutInterval = 10.0;

// POSTでhttp://test.com/api/testへ通信するタスクを作成します。
[manager POST:@"http://test.com/api/test"
   parameters:param
      success:^(NSURLSessionDataTask *task, id responseObject) {
          // 成功時(Status Code:200)の処理
          
          // responseObjectにレスポンスが格納されています。
          // アプリで使用するデータ形式にキャストして使って下さい。
          NSLog(@"%@", responseObject);
      } failure:^(NSURLSessionDataTask *task, NSError *error) {
          // 失敗時の処理
          NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
          NSLog(@"Status code = %dエラーが返ってきました。", response.statusCode);
          
          // このブロックではレスポンスオブジェクトが取得できません。
      }];

使用方法はAFHTTPRequestOperationManagerと大体同じですが、AFHTTPSessionManagerではリクエスト失敗時にレスポンスオブジェクトが取得できません。

AFHTTPRequestOperationManagerの方はfailureブロックでも引数のoperation.responseObjectからレスポンスオブジェクトが取得できます。

しかしながらAPI仕様で401(認証エラーを表すステータスコード)の時にエラー詳細などを返したい場合があると思います。

具体的なAPI仕様としては以下のようなものが該当します。

API仕様(仮)

リクエスト
http://test.com/api/test
method POST
param
{
  user_id : ユーザーID,
  password : パスワード
}

レスポンス

Status-code 200 OK
Content-type application/json

body
{
  user_character_info : ユーザー所有キャラクター情報,
  belogsto : 所属,
  friend_list : <現在はnullが返ります, 次期フェーズ実装予定>
}

Status-code 401 Unauthorized
Content-type application/json

body
{
  error_code : エラーコード
}

エラーコード内訳
401E100 = 認証の有効期限切れ 要再ログイン
401E101 = アカウントの有効期限切れ 要再登録
401E102 = アカウントの停止 ログイン停止中のユーザーアカウントからリクエストがあった

上記の仕様でAFHTTPSessionManagerで通信を行っていると、「401ステータス(認証エラー)なのはわかったけど、リクエストを送信したユーザーにどんな問題があったのか」までは知る事ができません。

レスポンスのbody部にあたる”error_code : エラーコード”を取得して、そのエラーの理由に適したUI表示をユーザーに行いたいとします。

アラートで認証エラー「再ログインして下さい。」などと表示しても良いのですが、ユーザーにとっては不親切でしょう。

ダウンロードで待ってる間にアプリをバックグラウンドに移した → ダウンロードが終わりそうな頃にアプリをフォアグラウンドに戻した → ダウンロードエラーが起こったのか再度0%から始まった

(アラート)「通信エラー」ダウンロードが中断されました。はじめからやり直して下さい。

こんなことがあれば「え、なんで?」と思われる方が多いと思います。

認証の有効期限切れがわかった段階で内部的に再ログイン処理を行っていれば、問題なく処理が継続できていたかもしれません。

そこで少し荒技ですが、successブロックに通してしまえばレスポンスオブジェクトが取得できるということを利用してステータスコード401をこのAPIでのみsuccess扱いにしてしまいましょう。

AFHTTPSessionManagerの実装例(401エラー対応版)

修正したのが以下です。

// successブロックで処理するステータスコードを指定します。
/**
  401レスポンスからbodyを取得したいため
  200レスポンスと401レスポンスをsuccessとみなします。
 */
NSMutableIndexSet *successCodes = [NSMutableIndexSet indexSetWithIndex:200];
[successCodes addIndex:401];
manager.responseSerializer.acceptableContentTypes = successCodes;

// POSTでhttp://test.com/api/testへリクエストを送信します。
[manager POST:@"http://test.com/api/test"
   parameters:param
      success:^(NSURLSessionDataTask *task, id responseObject) {
          // エラーが存在するかチェック
          id errorCode = [responseObject valueForKey:@"error_code"];
          if (errorCode && [errorCode isKindOfClass:[NSString class]]) {
              NSString *errorDetail = errorCode;
              if ([errorDetail isEqualToString:@"401E100"]) {
                  // 認証の有効期限切れです。
                  // 再ログインしてください。
              } else if ([errorDetail isEqualToString:@"401E101"]) {
                  // アカウントの有効期限切れです。
                  // アカウントの有効期限が切れました。新規アカウントを作成して下さい。
              } else if ([errorDetail isEqualToString:@"401E102'"]) {
                  // アカウント停止。
                  // あなたはプレイ出来ません。
              }
              return;
          }
          
          // 通常成功時の処理
          NSLog(@"%@", responseObject);
      } failure:^(NSURLSessionDataTask *task, NSError *error) {
          // 失敗時の処理
          NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
          NSLog(@"Status code = %dエラーが返ってきました。", response.statusCode);
      }];

色々試してみたのですが、この方法の他にresponseSerializerを継承していじるくらいしか解決できませんでした。何かもっと良い方法をご存知の方は是非コメント下さい。

こうすればAFHTTPSessionManagerで401ステータスコード(通常時はfailure扱い)が返ってきてもsuccessブロックでレスポンスオブジェクトが取得でき、エラー詳細にあった適切な処理が行えます。

実際のプロジェクトで実装するときは、他の方が見てもわかりやすいようにfailureの引数NSError *errorのuserInfoなどにエラーコードなどを格納してfailureブロックに処理を渡してあげるといった実装にすると良いかもしれません。

successCodesに200を含めないと、今まで成功していた200 OKステータスコードもfailureブロックで処理してしまうので注意して下さい。

まとめ

今回はAFHTTPSessionManagerのエラーステータスコードが返って来た時に、レスポンスオブジェクトを取り出す方法について書きました。

iOS 7の普及率が9割を超えたこともあり、今後iOS 8も登場することからiOS 6のサポートを切ることが増えて来ます。

iOS 7から使えるNSURLSessionDataTask、AFNetworkingではAFHTTPSessionManagerを使う機会はこれから増えてくると思いますので、参考になれば幸いです。