1 前回のフォロー

前回の記事でリリースしたコードに不具合を見つけました。Tipsでもあるので説明します。 初期画面から文書名をタップするとページのイメージが表示されますが、よく見るとナビゲーションバーの下辺にページの上辺がくっついて表示されています。本来は、ページの上辺はステータスバー(時計や電波状態が表示されている部分)の下辺から表示され、ナビゲーションバーと重なる部分はナビゲーションバーの下に透過して表示されるようになっていないといけません。 いろいろ調べたのですが、私がとった方法は、ナビゲーションバーのビュー階層の直下にスクロールビューを配置せず、間にUIViewを挟む方法です。



なぜこのようなコードが必要かを推測するに、どうやらNavigationControllerが初期画面を表示する際、初期画面がスクロール可能、すなわち、UIScrollViewを継承するビューの場合に、ビュー内部の表示内容がナビゲーションバーの領域に被さらないように、contentInsetをナビゲーションバーの高さだけ下に下げて表示させているようのですが、さらにpushViewControllerで次のViewを表示する際にも属性を引き継ぐかもしくは同じ調整を行っているようです。 これは、ちょっと余計なお世話な仕様のように思います。こういったことはフレームワークが暗黙的にやるより、フレームワークはデフォルトではcontentInsetを調整せずに表示して、UIScrollViewのドキュメントに、「ナビゲーションバーと併用するときは、.contentInsetプロパティで表示位置を調整してね」って書いてあった方がかえって混乱が少なくなる気がします。フレームワーク設計の常として、暗黙的にやってあげるべきことと、ユーザの判断でやってもらった方が良いことの線引きはなかなか難しいですね。

2 マウスイベントのハンドリング

さて、本題に入ります。前回でページイメージの表示を実現しましたが、まだ文書の1ページ目までしか表示出来無い状態でした。今回はタップイベントをハンドリングして、ページ送りを行えるようにします。 今回は、画面の左右両側1/3の領域をタップした場合に、ページを前後させ、中央部分をタップしたらナビゲーションバーの表示/非表示を切り替えるという仕様で考えます。



タップイベントのハンドリングにはiOS3.2、すなわちiPadリリースとともにサポートされたUIGestureRecognizerファミリのクラスを用います。 viewDidLoaddでUITapGestureRecognizerを作成して登録します。initWithTaget:action:の引数として、それぞれ、selfと、@selector(handleTapFrom:)を与えることで、タップ操作が起きたらPageViewController宛にhandleTapFrom:を送るよう指定します。

    1   UITapGestureRecognizer *recognizer;
    2   // UITapGestureRecognizerの作成と登録
    3   // recognizerが認識したイベントは自分宛をhandleTapFrom:送ってくるように設定
    4   recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapFrom:)];
    5   [self.view addGestureRecognizer:recognizer];
    6   [recognizer release];

ユーザが画面をタップすると送られるhandleTapFrom:を実装します。とりあえず今回は、左をタップすると次ページ、右をタップすると前ページを表示するように実装します。


    1 - (void)handleTapFrom:(UITapGestureRecognizer *)recognizer {
    2
    3   CGPoint location = [recognizer locationInView:nil]; // nilを指定するとスクリーン座標が取得できる
    4   LogPoint(location);
    5   UIWindow* window = [[UIApplication sharedApplication] keyWindow];
    6
    7   if( window.frame.size.width / 3 * 2 < location.x){ // 右
    8     Log("-->");
    9     if( document_.currentNombre > 1 ){
   10       document_.currentNombre--;
   11     }
   12     [self renderPage];
   13   }else if( location.x <= window.frame.size.width / 3 ){ // 左
   14     Log("<--");
   15     if( document_.currentNombre < [document_ pageCount] ){
   16       document_.currentNombre++;
   17     }
   18     [self renderPage];
   19   }
   20   else {// 真ん中
   21     Log(@"真ん中");
   22   }
   23
   24   [self renderPage];
   25
   26   [self.view setNeedsDisplay];
   27 }

見たまんまのコードですが、3行目、locationInView:でnilを渡すと、スクリーン座標系のタップ位置が取得できます。locationInView:はUIGestureRecognizerを設定したビューの階層関係にあるビューであればどのビューの座標系にも変換してくれます。余談ですが、Windowsのマッピングモードで苦労した記憶がある筆者にとってはとても便利なメソッドに思えます。 実際にタップを実行すると、なめらかにイメージが切り替わって表示されるのがわかると思います。これは、前回紹介したように、CALayerを用いてCGImageオブジェクトを設定すると、即座に画像が切り替わるのではなく、CoreAnimationの機能で自動的にアニメーションするようフレームワーク側が処理します。これは"Implict Animation"(暗黙的アニメーション)と定義されています。暗黙的とは逆に、明示的にアニメーションすることも出来ます。後の回で、Core Animationを用いてページめくりのバリエーションを試してみたいと思います。

3 ナビゲーションバーの表示・非表示

次に画面の真ん中をタップすると、ナビゲーションバーを非表示にすることで、ページ全体を読めるようにします。 ついでに、タイマーを用いて、文書を開いてから一定時間経過した場合もナビゲーションバーを非表示にするようにします。 まず、ナビゲーションバーを出したり消したりするメソッド、toggleNavbarです。 基本的にはナビゲーションバーの状態を参照して、現在の状態と逆の状態を設定するだけですが、表示させた場合にのみタイマーをセットして一定時間経過したら勝手に消えるようにします。


 01  -(void)toggleNavbar {
 02    if( self.navigationController.navigationBarHidden ){ // ナビゲーションバーが消えている
 03      [self.navigationController setNavigationBarHidden:NO animated:NO];// 表示する
 04      [self setNavbarTimer];	// 表示した場合は非表示タイマーを設定する
 05    }else {// ナビゲーションバーが出ている
 06      [self.navigationController setNavigationBarHidden:YES animated:NO];
 07      [self unsetNavbarTimer];
 08      // 非表示にした場合は消しっぱなし
 09    }
 10  }

-(void) setNavbarTimer {
  [self unsetNavbarTimer]; 
  navbarTimer_ = [[NSTimer timerWithTimeInterval:kEraseMenuInterval target:self selector:@selector(navbarTimerFired:)
				       userInfo:nil repeats:NO] retain];
    [[NSRunLoop currentRunLoop] addTimer:navbarTimer_ forMode:NSDefaultRunLoopMode];
} 
タイマーを生成して、retainして持っておきます。 タイマーが発火したら、navbarTimerFired:を送るように指定しておきます。

- (void)navbarTimerFired:(NSTimer*)timer {
  [self unsetNavbarTimer];
  Log(@"%s %@", __FUNCTION__, timer);
  [self toggleNavbar];

}
navbarTimerFired:はタイマーを無効化して、toggleNavbarを呼び出しているだけです。 タイマーの無効化メソッド、unsetNavbarTimerは下記のコードです。invalidateしてタイマーを無効化し、タイマーはこのコントローラがretainしていたのでreleaseすることでタイマーを解放します。

- (void) unsetNavbarTimer {
  if(navbarTimer_){
    [navbarTimer_ invalidate];
    [navbarTimer_ release];
    navbarTimer_ = nil;
  }
}
忘れがちですが、ビューそのものが非表示になるタイミングで送られるviewWillDisappear:メソッドの処理において、タイマーの解放のためにunsetNavbarTimerを呼び出すことと、反対に、ビューが表示されるタイミングで送られるviewWillAppear:中ではsetNavbarTimerを呼び出すようにします。 最後に、handleTapFrom:メソッドで画面中央部がタップされた場合のコードを修正し、

   20   else {// 真ん中
   21     [self toggleNavbar];
   22   }
として呼び出します。これで、ナビゲーションバーの表示/非表示をサポートできました。

4 まとめ

今回、基本的なタップ操作をサポートしたことでようやくビューアらしくなってきました。次回は画面の拡大・縮小について説明します。 今回までのコードは

bzr branch -r article-3-release lp:~klabrd/jisuireader/trunk article-3-release
で取得してください。

5 前回までの目次

iPadで動く電子書籍アプリを作ってみる(1) iPadで動く電子書籍アプリを作ってみる(2)