iOSでHTTP通信 -- NSURLConnectionのまとめ
iOSでネットワーク通信をするのは初めてだったのでメモ。
iOS Developer Libraryに公式のチュートリアル(日本語版)があるので、まずはこれをこなすのが良いかと思います。
実験環境:
- MacBook Air 11-inch, Mid 2013
- Xcode 5.1.1
- iOS7 (Simulator)
基本
iOSもといCocoaではNSURLConnectionクラスを利用してHTTP通信を行うことが出来ます。このクラスですが、時代とともにメソッドが追加されており、今のところ以下の3通りの手段が実装されています。
initWithRequest:...
とデリゲートを使う方法。昔からある方法。sendSynchronousRequest:...
で同期的に取得する方法。sendAsynchronousRequest:...
とブロックで非同期的に取得する方法。
NSRequestに関して
どの方法でも、NSRequestというリクエスト情報を詰め込んだオブジェクトを作成する必要があるので、ここで取り上げておきます。
NSRequestは、HTTPのリクエストを抽象化したクラスです……が、抽象度は低めです。そのため、例えばクエリに関しては、GETにしろPOSTにしろ自力で構成する必要があります。文字列のエンコードも手動で行う必要があります。文字列のエンコードはNSStringのstringByAddingPercentEscapesUsingEncoding:
を使うことで……と見せかけて*1、CFURLCreateStringByAddingPercentEscapes
という、いずれにしてもあまりに名前が長すぎて使う気が失せるメソッドで実現できます。
NSString* encodeURI(NSString *str) { return (NSString *)CFURLCreateStringByAddingPercentEscapes( kCFAllocatorDefault, (CFStringRef)str, NULL, CFSTR(":/?#[]@!$&'()*+,:="), kCFStringEncodingUTF8); }
この記事ではGETだけ扱いますが、POSTの場合はHTTPBodyにデータを入れる必要があります。細かい話はマニュアルに丸投げします。
デリゲートを使う方法
大雑把な流れは以下の通りです。
- NSConnectionDataDelegateプロトコルを採用し、メソッドを適当に実装。
- NSURLなどでNSRequestを作成。
- NSURLConnectionの
initWithRequest:delegate:
で実行。
initWithRequest:delegate:
ですが、インスタンス化した時点で通信が開始する点には注意が必要です*2。initWithRequest:delegate:startImmediately:
でstartImmediatelyにNOを渡せば、明示的に開始することも出来ます(start
メソッドで開始)。
NSURLConnectionDataDelegateに関して
NSURLConnectionに関連したプロトコルとして、NSURLConnectionDelegate、NSURLConnectionDataDelegate、NSURLConnectionDownloadDelegateというのがマニュアルに出てくるのですが、基本的にはNSURLConnectionDataDelegateだけ採用すればOKです。
もう少し詳しく書くと、
- NSURLConnectionDelegateはNSURLConnectionDataDelegateとNSURLConnectionDownloadDelegateの共通部分をまとめたプロトコル。
- NSURLConnectionDataDelegateとNSURLConnectionDownloadDelegateは、いずれもNSURLConnectionDelegateを採用している。
- NSURLConnectionを使うときはNSURLConnectionDataDelegateを、Newsstand Kitの
downloadWithDelegate:
を使うときはNSURLConnectionDownloadDelegateを採用する。
とのことです。とにかく、NSURLConnectionを使うときはNSURLConnectionDataDelegateを採用しておけば良いでしょう。
サンプルプログラム
適当なラッパークラスを作ったので載せておきます。
// WebRequest.h #import <Foundation/Foundation.h> @interface WebRequest : NSObject <NSURLConnectionDataDelegate> + (void)getURL:(NSString *)url block:(void (^)(NSData *))block; + (void)getURLAsJSON:(NSString *)url block:(void (^)(id))block; @end
// WebRequest.m #import "WebRequest.h" @implementation WebRequest { NSMutableData *_receivedData; void (^_callback)(NSData *); } + (void)getURL:(NSString *)url block:(void (^)(NSData *))block { return [[self alloc] getURL:url block:block]; } - (void)getURL:(NSString *)url block:(void (^)(NSData *))block { NSLog(@"GET %@", url); _callback = [block copy]; // IMPORTANT _receivedData = [NSMutableData dataWithCapacity:0]; NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:url] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:30.0]; NSURLConnection *connection = [NSURLConnection connectionWithRequest:req delegate:self]; if (!connection) { block(nil); _receivedData = nil; _callback = nil; } } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { NSLog(@"connection:didFailWithError"); _callback(nil); _receivedData = nil; _callback = nil; } - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { NSLog(@"connection:didReceiveResponse"); [_receivedData setLength:0]; } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { NSLog(@"connection:didReceiveData"); [_receivedData appendData:data]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { NSLog(@"connectionDidFinishLoading"); _callback(_receivedData); _receivedData = nil; _callback = nil; } #pragma mark - Additional Methods + (void)getURLAsJSON: (NSString *)url block:(void (^)(id))block { [WebRequest getURL:url block:^(NSData *data) { NSError *error = nil; id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (error) { NSLog(@"%@", error); block(nil); return; } // NOTE: // [json respondsToSelector:@selector(objectForKey:)]) // ? (json is a NSDictionary) // : (json is a NSArray) block(json); }]; } @end
// main.c // このブログのRSSをJSONで取得して、 // 最新エントリ3件の日付とタイトルを表示するプログラム #import "WebRequest.h" NSString* encodeURI(NSString *str) { return (NSString *)CFURLCreateStringByAddingPercentEscapes( kCFAllocatorDefault, (CFStringRef)str, NULL, CFSTR(":/?#[]@!$&'()*+,:="), kCFStringEncodingUTF8); } void printRecentEntries(void (^finish)()) { NSString *paramURL = encodeURI(@"http://shin.hateblo.jp/rss"); NSString *url = [NSString stringWithFormat: @"https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=3&q=%@", paramURL]; [WebRequest getURLAsJSON:url block:^(NSDictionary *data) { if (!data) { NSLog(@"Error: network error or invalid request."); finish(); return; } int status = [data[@"responseStatus"] intValue]; if (status != 200) { NSLog(@"Response Error: %d - %@", status, data[@"responseDetails"]); finish(); return; } NSArray *entries = data[@"responseData"][@"feed"][@"entries"]; for (int i = 0; i < [entries count]; ++i) { NSDictionary *entry = entries[i]; NSString *date = entry[@"publishedDate"]; NSString *title = entry[@"title"]; NSLog(@"[%@] %@", date, title); } finish(); }]; } int main(int argc, char* argv[]) { @autoreleasepool { __block BOOL finished = NO; printRecentEntries(^{ finished = YES; }); while (!finished) { [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]]; } NSLog(@"Finish!"); } return 0; }
何もしないと通信終了を待たずに終わってしまうので、実行ループ(Run Loop)で待っています。
以下、コンパイルと実行です。
$ clang main.m WebRequest.m -framework Foundation $ ./a.out 2014-05-04 15:03:42.035 a.out[50649:507] GET https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=3&q=http%3A%2F%2Fshin.hateblo.jp%2Frss 2014-05-04 15:03:43.166 a.out[50649:507] connection:didReceiveResponse 2014-05-04 15:03:43.167 a.out[50649:507] connection:didReceiveData 2014-05-04 15:03:43.167 a.out[50649:507] connection:didReceiveData 2014-05-04 15:03:43.167 a.out[50649:507] connection:didReceiveData 2014-05-04 15:03:43.168 a.out[50649:507] connection:didReceiveData 2014-05-04 15:03:43.168 a.out[50649:507] connection:didReceiveData 2014-05-04 15:03:43.168 a.out[50649:507] connectionDidFinishLoading 2014-05-04 15:03:43.169 a.out[50649:507] [Wed, 30 Apr 2014 06:16:58 -0700] Apacheでシンボリックリンクを使うときに注意すべき点 2014-05-04 15:03:43.170 a.out[50649:507] [Wed, 30 Apr 2014 05:43:11 -0700] 現在開いているファイルの入ったディレクトリ 2014-05-04 15:03:43.170 a.out[50649:507] [Tue, 29 Apr 2014 05:13:17 -0700] Perlにおけるコマンドライン引数の処理 2014-05-04 15:03:43.546 a.out[50649:507] Finish!
send(As|S)ynchronousRequestを使う方法
デリゲートを使う方法は、細かい制御が利く分、何かと面倒ですが、send(As|S)ynchronousRequestを使うと、もっとサックリと書けます。
あんまり説明することもないのでコード載せます。
sendSynchronousRequest
sendSynchronousRequestはスレッドを止めるので、UIがあるようなプログラムでは、スレッドを別に作成する必要があります。
#import <Foundation/Foundation.h> NSString* encodeURI(NSString *str) { return (NSString *)CFURLCreateStringByAddingPercentEscapes( kCFAllocatorDefault, (CFStringRef)str, NULL, CFSTR(":/?#[]@!$&'()*+,:="), kCFStringEncodingUTF8); } void printRecentEntries() { NSString *paramURL = encodeURI(@"http://shin.hateblo.jp/rss"); NSString *url = [NSString stringWithFormat: @"https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=3&q=%@", paramURL]; NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:url] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:30.0]; NSError *error; NSURLResponse *res; NSData *data = [NSURLConnection sendSynchronousRequest:req returningResponse:&res error:&error]; if (error) { NSLog(@"%@", error); return; } NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (error) { NSLog(@"%@", error); return; } int status = [json[@"responseStatus"] intValue]; if (status != 200) { NSLog(@"Response Error: %d - %@", status, json[@"responseDetails"]); return; } NSArray *entries = json[@"responseData"][@"feed"][@"entries"]; for (int i = 0; i < [entries count]; ++i) { NSDictionary *entry = entries[i]; NSString *date = entry[@"publishedDate"]; NSString *title = entry[@"title"]; NSLog(@"[%@] %@", date, title); } } int main(int argc, char* argv[]) { @autoreleasepool { printRecentEntries(); } return 0; }
sendAsynchronousRequest
sendAsynchronousRequestは内部的に、GDCでスレッドを立てて、sendSynchronousRequestを呼んでいるだけのようです(詳しくはこちらを参照)。
#import <Foundation/Foundation.h> NSString* encodeURI(NSString *str) { return (NSString *)CFURLCreateStringByAddingPercentEscapes( kCFAllocatorDefault, (CFStringRef)str, NULL, CFSTR(":/?#[]@!$&'()*+,:="), kCFStringEncodingUTF8); } void printRecentEntries(void (^finish)()) { NSString *paramURL = encodeURI(@"http://shin.hateblo.jp/rss"); NSString *url = [NSString stringWithFormat: @"https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=3&q=%@", paramURL]; NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:url] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:30.0]; NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [NSURLConnection sendAsynchronousRequest:req queue:queue completionHandler:^(NSURLResponse *res, NSData *data, NSError *error) { if (error) { NSLog(@"%@", error); finish(); return; } NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (error) { NSLog(@"%@", error); finish(); return; } int status = [json[@"responseStatus"] intValue]; if (status != 200) { NSLog(@"Response Error: %d - %@", status, json[@"responseDetails"]); finish(); return; } NSArray *entries = json[@"responseData"][@"feed"][@"entries"]; for (int i = 0; i < [entries count]; ++i) { NSDictionary *entry = entries[i]; NSString *date = entry[@"publishedDate"]; NSString *title = entry[@"title"]; NSLog(@"[%@] %@", date, title); } finish(); }]; } int main(int argc, char* argv[]) { @autoreleasepool { // ここでは処理終了を待つのにGCDのセマフォを使ってみたが、 // 最初の例のように実行ループで待っても多分OK。 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); printRecentEntries(^{ dispatch_semaphore_signal(semaphore); }); dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); dispatch_release(semaphore); } return 0; }