Skip to content

Instantly share code, notes, and snippets.

@akisute
Created June 17, 2010 02:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save akisute/441620 to your computer and use it in GitHub Desktop.
Save akisute/441620 to your computer and use it in GitHub Desktop.
#import <Foundation/Foundation.h>
@interface FetchOperation : NSOperation {
NSURLRequest *request;
NSURLConnection *connection;
NSHTTPURLResponse *response;
NSMutableData *responseBody;
NSOperation *parseOperation;
BOOL ready;
BOOL executing;
BOOL finished;
BOOL cancelled;
}
@property (nonatomic, copy) NSURLRequest *request;
- (void)parseResponseBody:(NSData *)bodyData;
@end
#import "FetchOperation.h"
#import "AppDelegate.h"
@implementation FetchOperation
#pragma mark Properties
@synthesize request;
- (void)setReady:(BOOL)b {
if (ready != b) {
[self willChangeValueForKey:@"isReady"];
ready = b;
[self didChangeValueForKey:@"isReady"];
}
}
- (void)setExecuting:(BOOL)b {
if (executing != b) {
[self willChangeValueForKey:@"isExecuting"];
executing = b;
[self didChangeValueForKey:@"isExecuting"];
}
}
- (void)setFinished:(BOOL)b {
if (finished != b) {
[self willChangeValueForKey:@"isFinished"];
finished = b;
[self didChangeValueForKey:@"isFinished"];
}
}
- (void)setCancelled:(BOOL)b {
if (cancelled != b) {
[self willChangeValueForKey:@"isCancelled"];
cancelled = b;
[self didChangeValueForKey:@"isCancelled"];
}
}
#pragma mark Init/dealloc
- (id) init {
if (self = [super init]) {
parseOperation = nil;
ready = YES;
executing = NO;
finished = NO;
cancelled = NO;
}
return self;
}
- (void) dealloc {
[request release];
[connection release];
[response release];
[responseBody release];
if (parseOperation) {
[parseOperation removeObserver:self forKeyPath:@"isFinished"];
[parseOperation removeObserver:self forKeyPath:@"isCancelled"];
[parseOperation release];
parseOperation = nil;
}
[super dealloc];
}
#pragma mark NSOperation
- (BOOL)isConcurrent {
// FetchOperation runs in concurrent mode, managed manually.
return YES;
}
- (void)start {
// Follows the behavior of NSOperation in Mac OS 10.6 (and iPhone OS 3.0)
// また、start前にrequestがセットされていない場合にも例外を発生させる
if (finished || cancelled) {
[self cancel];
return;
}
if (!ready || executing || !request) {
@throw NSInvalidArgumentException;
}
// サーバーと通信可能かどうかを確認する
// 通信出来ない場合には何もしないで終了する
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
if (!appDelegate.internetConnectionAvailable) {
NSLog(@"Device is not reachable to the API server. Aborting.");
[self cancel];
return;
}
// connectionの準備を行う。準備だけ。実行はmainで非同期的に。
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
if (!connection) {
@throw NSInvalidArgumentException;
}
// main実行直前に状態を変更する、その後実行。
[self setReady:NO];
[self setExecuting:YES];
[self setFinished:NO];
[self setCancelled:NO];
// mainのためにスレッドを作る必要がない、main自体が非同期関数だけで出来ているので
[self main];
}
- (void)cancel {
// 通信をキャンセルして、状態をキャンセル状態にする。
[connection cancel];
[self setReady:NO];
[self setExecuting:NO];
[self setFinished:YES];
[self setCancelled:YES];
}
- (BOOL)isReady {
return ready;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
- (BOOL)isCancelled {
return cancelled;
}
- (void)main {
// DO NOT override this method in subclasses. Use parseResponseBody instead.
// iOS4に対応するため、currentRunLoopではなくmainRunLoopを使う必要があります。
// ※iOS4はMac OS 10.6と同じNSOperationの挙動になるため。詳細はブログに書きます
NSLog(@" * Executing API %@", [self.request URL]);
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[connection start];
}
#pragma mark NSURLConnection
- (void)connection:(NSURLConnection*)sender didReceiveResponse:(NSHTTPURLResponse*)res {
NSLog(@" * Response received for API %@", [self.request URL]);
// didReceiveResponseは極稀にひとつの接続で複数回呼ばれることがあるので、その点に注意して書く。
// まずはレスポンスとボディを格納する準備をする。
if (response) {
[response release];
response = nil;
}
if (responseBody) {
[responseBody release];
responseBody = nil;
}
responseBody = [[NSMutableData alloc] init];
// レスポンスが飛んできたので確保する
// レスポンスが200以外であれば即座にキャンセルして終了する
response = [res retain];
if ([response statusCode] != 200) {
NSLog(@"Error while executing API: invalid HTTP status code %d has been returned", [response statusCode]);
[self cancel];
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
[appDelegate showConnectionAlertOnMainThreadWithMessage:@"エラーだよーん"];
return;
}
}
- (void)connection:(NSURLConnection*)sender didReceiveData:(NSData*)data {
// データが送られてきたら都度キャッシュに突っ込む
[responseBody appendData:data];
}
- (void)connection:(NSURLConnection*)sender didFailWithError:(NSError*)error {
// 接続に失敗したらキャンセル状態にして、タイムアウトエラーメッセージを出す
[self cancel];
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
[appDelegate showConnectionAlertOnMainThreadWithMessage:@"接続失敗だよーん"];
}
- (void)connectionDidFinishLoading:(NSURLConnection*)sender {
NSLog(@" * Data load completed for API %@", [self.request URL]);
// parseMainを別スレッドで起動して続きを行う
// スレッドの完了通知を受け取ってからこのオペレーションを終了させること
parseOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(parseMain:) object:nil];
[parseOperation addObserver:self
forKeyPath:@"isFinished"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:NULL];
[parseOperation addObserver:self
forKeyPath:@"isCancelled"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:NULL];
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
[appDelegate.parseOperationQueue addOperation:parseOperation];
}
- (NSURLRequest *)connection:(NSURLConnection *)sender willSendRequest:(NSURLRequest *)req redirectResponse:(NSURLResponse *)res {
// リダイレクト要請は常に受け入れてリダイレクトする
return req;
}
#pragma mark Method
- (void)parseResponseBody:(NSData *)bodyData {
// Default implementation does nothing.
// Override this method to parse the response data.
return;
}
#pragma mark NSThread
- (void)parseMain:(id)sender {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
// すべての通信が完了したので、responseBodyのチェック。responseBodyがnullであれば何らかのエラーがあったと考えられるのでキャンセルして終了。
if (!response || !responseBody) {
NSLog(@"Error while executing API: Neither response nor body are returned from the server.");
[self cancel];
[appDelegate showConnectionAlertOnMainThreadWithMessage:@"レスポンスが帰ってこなかったよーん"];
return;
}
// チェック完了。responseBodyを渡してparseする。
// parseResponseBodyは同期的に、かつロックを掛けて実行されるので注意。ロックの解放はfinallyで行う。
@try {
[appDelegate.managedObjectContext lock];
[self parseResponseBody:responseBody];
}
@catch (NSException *e) {
// DO NOT throw exceptions out from NSOperation in order not to stop the entire application.
NSLog(@"Error while parsing API response: %@", e);
}
@finally {
[appDelegate.managedObjectContext unlock];
// パースも完了、すべての処理が終了。pool releaseはfinallyに入れておかないとエラー時に実行されなくなりメモリが漏れる
[pool release];
}
}
#pragma mark -
#pragma mark NSKeyValueObserving
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// ParseOperationの状況に応じて自身の状況を変化させる
if ([keyPath isEqual:@"isFinished"]) {
[self setReady:NO];
[self setExecuting:NO];
[self setFinished:YES];
[self setCancelled:NO];
} else if ([keyPath isEqual:@"isCancelled"]) {
[self cancel];
}
// ParseOperationを開放する
[parseOperation removeObserver:self forKeyPath:@"isFinished"];
[parseOperation removeObserver:self forKeyPath:@"isCancelled"];
[parseOperation release];
parseOperation = nil;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment