1. akitaika_

    Posted

    akitaika_
Changes in title
+iOS 弱者が NSURLConnection を NSURLSession に置き換えた話
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,226 @@
+# はじめに
+
+こんにちは!画面に向かってこんにちは!
+
+「Android できるなら、iOS もいけるよね?」
+なーんて言われるのは、スマホ系エンジニアの世の常です。
+
+そんなこんなで、6月くらいから iOS に__強制的に__手を出させられたわけですが、
+今回は、iOS 9 で deprecated になってしまった NSURLConnection を NSURLSession に置き換えたメモです。
+
+# やりたいこと
+
+ * 既存のHTTP通信の処理を__大きく変更することなく__置き換える
+ * __ATSの話には触れない__(それはそれ、これはこれ)
+
+※ ソースコードは Objective-C です。
+
+# NSURLConnection を NSURLSession に置き換える
+
+まずはコードから。
+
+## Before:呼び出し
+
+```objc
+// HTTPリクエスト
+NSURLConnection *connect = [[NSURLConnection alloc] initWithRequest:request delegate:self];
+if (!connect) {
+ // HTTPリクエスト失敗処理
+ [self failureHttpRequest:nil];
+}
+```
+
+## Before:デリゲート
+
+```objc
+/**
+ * HTTPリクエストのデリゲートメソッド(データ受け取り初期処理)
+ */
+- (void)connection:(NSURLConnection *) connection didReceiveResponse:(NSURLResponse *) response {
+ // 保持していたレスポンスのデータを初期化
+ [self setResponseData:[[NSMutableData alloc] init]];
+}
+
+/**
+ * HTTPリクエストのデリゲートメソッド(受信の度に実行)
+ */
+- (void)connection:(NSURLConnection *) connection didReceiveData:(NSData *)data {
+ // 1つのパケットに収まらないデータ量の場合は複数回呼ばれるので、データを追加していく
+ [_responseData appendData:data];
+}
+
+/**
+ * HTTPリクエストのデリゲートメソッド(成功処理)
+ */
+- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
+ // HTTPリクエスト成功処理
+ [self successHttpRequest];
+}
+
+/**
+ * HTTPリクエストのデリゲートメソッド(失敗処理)
+ */
+- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
+ // HTTPリクエスト失敗処理
+ [self failureHttpRequest:error];
+}
+```
+
+## After:呼び出し
+
+```objc
+// HTTPリクエスト
+NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
+NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration
+ delegate:self
+ delegateQueue:[NSOperationQueue mainQueue]];
+NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
+[task resume];
+```
+
+## After:デリゲート
+
+```objc
+/**
+ * HTTPリクエストのデリゲートメソッド(データ受け取り初期処理)
+ */
+- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
+ didReceiveResponse:(NSURLResponse *)response
+ completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
+ // 保持していたレスポンスのデータを初期化
+ [self setResponseData:[[NSMutableData alloc] init]];
+
+ // didReceivedData と didCompleteWithError が呼ばれるように、通常継続の定数をハンドラーに渡す
+ completionHandler(NSURLSessionResponseAllow);
+}
+
+/**
+ * HTTPリクエストのデリゲートメソッド(受信の度に実行)
+ */
+- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
+ // 1つのパケットに収まらないデータ量の場合は複数回呼ばれるので、データを追加していく
+ [_responseData appendData:data];
+}
+
+/**
+ * HTTPリクエストのデリゲートメソッド(完了処理)
+ */
+- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
+ if (error) {
+ // HTTPリクエスト失敗処理
+ [self failureHttpRequest:error];
+ } else {
+ // HTTPリクエスト成功処理
+ [self successHttpRequest];
+ }
+}
+```
+
+## 解説
+
+NSURLSession のデリゲートですが、
+NSURLSessionTaskDelegate と NSURLSessionDataDelegate を使っています。
+NSURLSessionTaskDelegate の方は NSURLSession を扱う時の共通処理みたいな感じなので、
+用途や雰囲気に応じて、タスクと NSURLSessionDataDelegate の方を変更していくことになると思います。
+
+ここで重要なポイントは、2つあります。
+
+まず1つ目は、
+
+```objc
+NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration
+ delegate:self
+ delegateQueue:[NSOperationQueue mainQueue]];
+```
+
+の delegateQueue です。
+
+NSURLConnection のデリゲートメソッドはメインスレッドで処理されています。
+ところがどっこい、NSURLSession の方は指定しなければ、__メインスレッドで処理されません__。
+なので、例えば、コードの例で出している successHttpRequest メソッドの中で、
+ビューの生成処理なんぞをしてようものなら、容赦なく EXC_BAD_ACCESS になります。
+よって、メインスレッドで実行してもらうために、メインキューを指定します。
+
+今回のコンセプトは、「元のコードを大きく変えない」なので、
+このように修正しましたが、本来はここでキューを指定するのではなく、
+didCompleteWithError でメインスレッドでの処理をキックするような処理を書いた方がスマートだと思います。
+
+もしくは、見た目キレイに見えないけど、こう書くとか。
+
+```objc
+/**
+ * HTTPリクエストのデリゲートメソッド(完了処理)
+ */
+- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (error) {
+ // HTTPリクエスト失敗処理
+ [self failureHttpRequest:error];
+ } else {
+ // HTTPリクエスト成功処理
+ [self successHttpRequest];
+ }
+ });
+}
+```
+
+個人的にこう書くのは、Android で言うところの
+
+```java
+new Handler(Looper.getMainLooper()).post(new Runnable()
+ @Override
+ public void run() {
+ // UI の処理
+ }
+});
+```
+
+みたいな感じで好きじゃないんですけどね…。
+
+そして、2つ目は didReceiveResponse メソッドの
+
+```objc
+completionHandler(NSURLSessionResponseAllow);
+```
+
+コイツです。
+これを呼ばないと、didReceiveData と didCompleteWithError が呼ばれません。
+(didReceiveResponse メソッドをオーバーライドしなければ、呼ばれます)
+意味合い的には、「処理を継続するよ!」というのをタスクに伝えている感じです。
+
+今回はいらなかったので省いてますが、他にもキャンセルとかダウンロード開始とかあるので、
+ステータスコードとかを見て、その辺を振り分けるのが適切かと思います。
+
+# 余談
+
+機内モードとか通信できない時のエラー概要の文言が微妙に変わってます。
+NSURLConnection 利用時は、
+
+```
+Error Domain=NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline."
+```
+
+だったのに、NSURLSession 利用時は、
+
+```
+Error Domain=NSURLErrorDomain Code=-1009 "The operation couldn’t be completed. (NSURLErrorDomain error -1009.)"
+```
+
+前者の方がわかりやすいと思うけどなぁ…。
+
+# まとめ
+
+ * NSURLConnection は deprecated になっちゃったから、NSURLSession に置き換えよう!
+ * みんな NSURLSession 関連の話をバックグラウンド処理の話しかしてくれないからムシャクシャして書いた、反省はしている
+ * 他にも困ったことがあったので、そのうち書きます(UIWindow の回転の話とかね!)
+
+# 参考
+
+ * [iOS]2015年9月16日リリースのiOS9対応とipv6移行対応について対応と参考資料まとめ
+ http://to-developer.com/blog/?p=2036
+ * [iOS 7 SDK] [NSURLSession] Custom Delegate を使用した NSURLSessionDataTask
+ https://hirooka.pro/?p=3223
+ * NSURLSessionDataDelegate
+ https://developer.apple.com/library/mac/documentation/Foundation/Reference/NSURLSessionDataDelegate_protocol/
+ * How you would use NSURLSession to download files
+ http://sweettutos.com/2014/09/16/how-you-would-use-nsurlsession-to-download-files/