ナビゲーションバーの戻るボタン長押しで一番上の階層に戻るようにしてみる

TweetBotだとつぶやきから他の人のプロフィール見てそのつぶやきみてっていうようにどんどん深い階層に降りていった時、左上の戻るボタンを長押しすると一番上の階層に戻ることができる。

普通のナビゲーションバーではそんなこと出来ないのでちょっとやってみた。

まずUINavigationControllerを継承してUILongTapGestureRecognizerをnavigationBarにセットする。

- (void)viewDidLoad
{
    [super viewDidLoad];
        UILongPressGestureRecognizer *longTap = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(backToRoot:)];
        longTap.delegate = self;
        [self.navigationBar addGestureRecognizer:longTap];
}

navigationBarに追加する理由は、中のビューに触れないので。
delegateを指定して戻るボタン以外のところではジェスチャーをキャンセルするようにする。

-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    UIView *v = self.navigationBar.subviews.lastObject;
    if ([[[v class] description].lowercaseString rangeOfString:@"itembutton"].location != NSNotFound) {
        CGPoint p = [gestureRecognizer locationInView:v];
        if (CGRectContainsPoint(v.bounds, p)) {
            return YES;
        }
    }
    return NO;
}

GestureRecognizerのdelegateに指定しているのでdelegateメソッドを実装する。
今のところ戻るボタンはUINavigationBarの一番上に置かれるようなので
navigationBarの一番上のビューを取得する。
これもまた今のところバックボタンはUINavigationBarItemButtonViewというプライベートなクラスなので
クラス名からitembuttonがあるかをチェックし(そのままクラス名を使うとリジェクトされそうなので)、
タップ位置がそのボタンの中に存在すればYESを返しGestureRecognizerを開始する。

あとは長押しの通知を受けたら一番上の階層に戻すメソッドを実装するだけ

-(void) backToRoot:(UIGestureRecognizer *)recog
{
    if (recog.state == UIGestureRecognizerStateBegan) {
        [self popToRootViewControllerAnimated:YES];        
    }
}

まあ個人的には4階層以上深い階層になるんだったらアプリの設計を考え直したほうが
良いと思ってる口なんですが、やむを得ずそういう仕様になったときは
こういう小ネタを仕込んどくと良いかもしれません。

UIWindow#windowLevelについて(ステータスバーの上にボタンを置く)

UIWindowは、Viewツリーの頂点にいて最初にアプリケーション側が1個作るので、
自分で作るということはめったにないのだが、この中のプロパティwindowLevelが気になったので
ちょっと調べてみた。

windowLevelはUIWindowLevel型のプロパティである。
UIWindowLevelの中身はCGFloatだった。

もともと定義してある
UIWindowLevelNormal, UIWindowLevelStatusBar, UIWindowLevelAlertは見てみると
0.0, 1000.0, 2000.0 だった。

てことはもしかして
数字がでかいほど前に表示されるんじゃね?と思い、
例えばwindowLevelを1000〜にすると
ステータスバーの前に色々好きなもの置けるのではないかと思ったわけです。

で、やってみた

window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    window.windowLevel = 1001.0f;
    window.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5f];
    
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [btn setTitle:@"hoge" forState:UIControlStateNormal];
    [btn sizeToFit];
    
    [window addSubview:btn];
    [window makeKeyAndVisible];

でけた。

そら案内の注意報ボタンとかバッテリーマークのところに出てくるけど、
こんなかんじでやってるのかなあと思いました。

NSNotificationCenter#addObserverForName:object:queue:usingBlock:が便利

NSNotoficationCenterといえば通知を出したり受けるためによく使われるクラスですが、
通知を受けるためには今までobserverとselecterを指定する必要がありました。

それを一箇所でまとめてできるのが
NSNotificationCenter#addObserverForName:object:queue:usingBlock: です。
iOS5からだと思ってたらiOS4からでした。

        id observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"somethingNotification"
                                                                     object:nil
                                                                      queue:nil
                                                                 usingBlock:^(NSNotification *note) {
                                                                     NSLog(@"do something");
                                                                 }];

とこのように書けます。
引数は
1:通知名(NSString *)
2:通知を出すオブジェクト(id)
3:ブロックを実行するキュー(NSOperationQueue *)
4:実行するブロック

で返り値はこれを実行するオブザーバーです。
返り値を見るとNSObserverというクラスっぽいのですが、このクラスは公開されていないのでid型で受け取ります。

肝は第3引数です。
メインスレッドでブロックを実行したい場合はNSOperationQueue#mainQueueかnilを指定します。
別スレッドで実行したい場合は適当にNSOperationQueueのインスタンス突っ込めばいいでしょう。

通知を受け取るのを辞めたい場合は返り値でうけとったオブザーバーをremoveObserverすれば良いです。

[[NSNotificationCenter defaultCenter] removeObserver:observer];

通知をつけたり外したりといったことがあんまりないものについてはこっちで書いたほうがサクっと通知を作りやすいですね!

TWTweetComposeViewControllerの初期カーソル位置を一番最初に持っていく方法

iOS5で登場したTWTweetComposeViewControllerで簡単にTwitter投稿機能を実装出来るようになったんですが、
initialTextを入れた際、最初のカーソル位置がいちばんうしろに来てしまう。

initialTextでハッシュタグか何かを入れる場合が多いので、できれば表示された段階で一番前にカーソル位置があって欲しいと思うわけです。

最初にカーソルが欲しい

Viewを調べるとテキスト入力部分はUITextViewであることがわかりました。
従ってfirstResponderになっていると想像できます。
firstResponderを捜す方法はググったらすぐ出てきました。

http://stackoverflow.com/questions/1823317/how-do-i-legally-get-the-current-first-responder-on-the-screen-on-an-iphone

これを追加してあとはカーソル位置を最初に持っていくだけです。
iOS5からUIViewController#presentViewController:animated:completionメソッドのおかげでモーダル表示後に
処理を加えるのが楽になりました。

- (IBAction)tweet:(id)sender 
{
    TWTweetComposeViewController *tvc = [[TWTweetComposeViewController alloc] init];
    [tvc setInitialText:@"#ハッシュタグ"];
    [self presentViewController:tvc
                       animated:YES
                     completion:^{
                         UITextView *textView = (UITextView *)[[[UIApplication sharedApplication] keyWindow] findFirstResponder];
                         textView.selectedRange = NSMakeRange(0, 0);
                     }];
}

頑張ってブログやってみる。

本当ははてなブログにしようと思ったんだけど、
スーパーpre記法でコードシンタックスがまだ仕事しないようなのでこっちで書こうと思う。
完全に売名行為です。申しわけございません。

Obj-CのコードはだいたいARC有効のコードになると思います。

今まで何回も続かなかったのですが、三日坊主にならないように出来るだけ頑張ります。