yo_waka's blog

418 I'm a teapot

UINavigationControllerのタップ制御がムズい

使い勝手のためにコンテンツの表示領域を広く取れるように、タブバーをスクロール時に閉じて、ナビゲーションバー含む画面領域タップで再表示するようにしたい。
なので、UINavigationControllerをUITapGestureRecognizerでタップ制御しようとしたんだけど、予想外にめんどくさかった。

- (void) setScrollGesture: (UIView *)view
{
    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(handleTap:)];
    view.userInteractionEnabled = YES;
    [view addGestureRecognizer: tapGesture];
}

- (void) handleTap: (id)sender
{
    [self showTabBar: YES];
}

// NavigationControllerにジェスチャを設定
[self setScrollGesture: self.navigationController];

こうすると戻るボタンもジェスチャにイベントが奪われて実行されなくなってしまう。
navigationBar.subViewsからUIButtonのみ除いてジェスチャをセットしたいのだけど、UINavigationController内の背景ビュークラス等はなんと非公開クラスのため"isKindOfClass"が使えない。。。

なので強引だけどsubViewのクラス名を文字列でチェックしてジェスチャに設定する。

- (void) viewDidAppear: (BOOL)animated
    NSArray *subViews = self.navigationController.navigationBar.subviews;
    UIView *navigationBgView = nil;

    for (UIView *subView in subViews) {
        if (subViews.count > 1) {
            if ([[[subView class] description].lowercaseString rangeOfString: @"uinavigationitemview"].location != NSNotFound) {
                navigationBgView = subView;
            }
        } else {
            if ([[[subView class] description].lowercaseString rangeOfString: @"uinavigationbarbackground"].location != NSNotFound) {
                navigationBgView = subView;
            }
        }
        if (navigationBgView) {
            break;
        }
    }
    if (navigationBgView) {
        [self setScrollGesture: navigationBgView];
    }
}

これでタイトル部分のみにジェスチャを効かすことができる。
ボタンが複数あるときはタイトル部分しかジェスチャ効かないけど、仕方ないか。
タイトルを表示しない場合はちょっと違うやり方を探したほうがよさそう。
ひょっとして他によいやり方あったりするのかな?


タブバーをアニメーション付きで下からニョキッと出したり隠したりするのはこちらの記事が参考になります
自分の環境でもUIViewControllerのhiddenTabBarにBOOL入れるだけじゃダメだった。

記事では画面の高さが決め打ちになっていてiPhone5に対応していないので、そこはアプリのFrameを使うように変えてあげればいい。

- (void) showTabBar: (BOOL)animated
{
    CGRect bounds = [[UIScreen mainScreen] applicationFrame];
    
    if (animated == YES) {
        [UIView beginAnimations: nil context: NULL];
        [UIView setAnimationDuration: 0.4];
    }
    
    for (UIView *view in self.tabBarController.view.subviews) {
        CGRect rect = view.frame;
        if([view isKindOfClass: [UITabBar class]]) {
            rect.origin.y = bounds.size.height - 29;
            [view setFrame: rect];
        } else {
            rect.size.height = bounds.size.height - 29;
            [view setFrame: rect];
        }
    }
    
    if (animated == YES) {
        [UIView commitAnimations];
    }
    
    self.hiddenTabBar = NO;
}

- (void) hideTabBar: (BOOL)animated
{
    CGRect bounds = [[UIScreen mainScreen] applicationFrame];
    
    if (animated == YES) {
        [UIView beginAnimations: nil context: NULL];
        [UIView setAnimationDuration: 0.4];
    }
    
    for (UIView *view in self.tabBarController.view.subviews) {
        CGRect rect = view.frame;
        if([view isKindOfClass: [UITabBar class]]) {
            rect.origin.y = bounds.size.height + 20;
            [view setFrame: rect];
        } else {
            rect.size.height = bounds.size.height + 20;
            [view setFrame: rect];
        }
    }
    
    if (animated == YES) {
        [UIView commitAnimations];
    }
    
    self.hiddenTabBar = YES;
}

Objective-CでHTTPリクエスト扱うライブラリ作った

sendAsynchronouseRequest個別に書くのはしんどいし、最初は簡単にラップしてBlocksでコールバック渡すのがいいかなーと思ったけどself渡すのにいちいち__weakつけて作るのがめんどくさくなった。
setTimeoutの関数に「var that = self」やるのがめんどくさいあんな感じ。

多分ほぼAPIとのやりとりになるiPhoneアプリのHTTP通信はシンプルでよくて、HTTPリクエストをセットできて個別にコールバックを書けてHTTPレスポンスを受け取れればいい。
あと最近はRESTなAPIも多いのでPUT/DELETEリクエストも使いたいところ。

JS脳なのでこんな感じで書きたいw

$.get(url, params, this.handleSuccess_, this.handleFailure);
$.getJSON(url, params, this.handleSuccess_, this.handleFailure);

NSURLRequestはパラメータをセットするのがとてもめんどくさいのでそこはラップするとして。

コールバックをBlocks以外でやるとデリゲートが真っ先に浮かぶわけですが、そうすると1対1になっちゃう。
なんかいいのないかなーと探したらNSInvocationてのがありました。
これを使うとあるオブジェクトにセレクタを引数渡して実行することができる。

こんな感じ。

// Invocationを作って
NSInvocation *invocation = nil;
NSMethodSignature *sig = [delegate methodSignatureForSelector: someSelector];
if (sig) {
    invocation = [NSInvocation invocationWithMethodSignature: sig];
    [invocation setTarget: delegate];
    [invocation setSelector: someSelector];
}

// 引数をセットして実行する
[invocation setArgument: firstArgument atIndex: 2];
[invocation setArgument: secondArgument atIndex: 3];
[invocation invoke];

これならデリゲートと成功/失敗コールバックを渡して中でNSInvocation作ってsendAsynchronousRequestのcompleteBlock内でよしなにコールバックを実行してあげればいい。


ということでHttpClientライブラリを作ってみた。
waka/CCHttpClient

こんな感じでデリゲートとセレクタを渡して使える。

#import "CCHttpClient"

- (void) get: (NSString *)url
{
    CCHttpClient *client = [CCHttpClient clientWithUrl: url];
    [client getWithDelegate: @{@"foo": @"bar"} // Query parameters as NSDictionary
                    headers: @{@"application/json": @"Accept"} // HTTP headers as NSDictionary
                   delegate: self
                    success: @selector(handleGetSuccess:result:)
                    failure: @selector(handleGetFailure:error:)];
}

- (void) handleGetSuccess: (NSHTTPURLResponse *)response result: (NSData *)result
{
    [SVProgressHUD: showSuccessWithStatus: @"Success!"];
    id json = [CCHttpClient responseJSON: result]; // If want JSON data
    [self updateView: json];
}

- (void) handleGetFailure: (NSHTTPURLResponse *)response result: (NSError *)error
{
    [SVProgressHUD: showErrorWithStatus: @"Failure!"];
}

同じようにPOST/PUT/DELETEも使えます。
JSONの扱いどうしようと思ったけど、そこはJSのXHRと同じくレスポンスをそのまま返しちゃって、アプリ側でJSONとして使えばいいかと。

Objective-Cはじめました

近々仕事で書くことになりそうなので先週からObjective-C勉強中。
新しい言語を覚えるのは楽しくていいですね。
StoryBoardでパーツペタペタ貼ってプロパティ設定してると昔触ったVB6を思い出します(遠い目


GUIを作るという意味ではJavaScriptやActionScript3と同じなので、考え方とかdelegeteとかのデザインパターンは特に問題ないのだけれど、やはりメモリ管理、特にBlocks使ってる箇所でEXC_BAD_ACCESS出まくったりで苦戦。。。
Blocksの中でself使ったらダメとか、上の関数で受け取った引数もそのまま入れるとダメ(な時がある)とか@propertyあまり使いこなせてない感とか、ちゃんと理解できるにはもう少しかかりそう><

Blocksとメモリ周りについて以下のブログが参考になりました。
IOS 開発で、EXC_BAD_ACCESS とさよならするための6つのルール
ARC : 循環参照


もっと慣れるためにもやっぱりアプリを作ってみないとね、ということでAPIからJSON受け取って表示するよくあるビューアーアプリを作ってみたのですが、Objective-CってHTTPリクエストのやりとりめちゃめんどくさくね?

毎回クエリーストリング文字列で作ったりURLエンコードするのめんどくさいし、URLConnectionのコールバックに渡すBlocks内でエラー判定してJSONデコードしたりとかコールバックが膨らみすぎてやばい。画面内で複数API叩いたりしてるとさらに倍増。

なのでもっと簡単にHTTPリクエストのやりとりが出来るライブラリを作ってみました。ザ・車輪。

コードはGitHubにあげました。久しぶりに個人用のGitHub使ったぜい。
waka/objc-Async
※ NSURLConnection#sendAsynchronousRequestを使ってるのでiOS5以上でないと動きません


中身はPromise/Aを実装したDeferredと、Deferredを使って非同期なGET/POSTリクエストを簡易化したHttpClientです。
JavaScriptでよくやるやり方ですね。
Deferredの実装は、以前JavaScriptでPromise/Aを実装したものObjective-Cで書き換えただけ!
Async下のソースファイルをコピーするだけで普通に使えます。

使い方はReadmeにも書いてあるけど、基本的にAPIのURLとNSDictionaryにしたURLパラメータを渡すとDeferredが返ってくるので、あとはHTTP通信後のコールバックとエラーバックをセットするだけ。

NSString *url = "http://path/to/api";
NSDictionary *params = @{@"username": @"waka", @"apiKey": @"secret"};

Deferred *deferred = [HttpClient doGet: url parameters: params];
[[deferred then: ^id(id resultObject) {
    // レスポンスのJSONはNSDictionaryに変換されて渡ってくる
    NSDictionary *data = (NSDictionary *)resultObject;
    NSLog(@"%@", data[@"hoge"]);
}] resolve: nil];


ユニットテストを書くためにGHUnitという非同期テストをサポートしているテスティングフレームワークを使ってみたのですが、これすごいいいですね。
何がいいってテストコードがアプリとしてビルドされるので、エミュレータないし実機でテストを走らせることができるのがいい。
おかげでLeaks使って簡単にメモリチェックできました。
導入もCocoaPods使うか、自分でmakeしてFrameworksに追加するだけなので簡単。
GUIのテストとユニットテストをこれ1つでできちゃいそうな感じなので便利に使っていこうと思います。