初めに

あけましておめでとうございます。ponpoko1968です。昨年中はご愛読ありがとうございました。2011年も若手ブログをよろしくお願いいたします。 さて、今回はズーム機能の説明です。

通常のズーム機能

これまでの記事で何度かご説明したように、今回のアプリでは、文書のページイメージを表示するビューをUIScrollViewのサブビューとするようビューの階層構造を作っています。 これは、UIScrollViewのズーム機能を活用することが目的の一つでした。 早速、ページ表示画面にズーム機能を追加してみましょう。 UIScrollVeiwは、UIScrollViewDelegateというプロトコルをサポートしており、UIScrollViewに対してユーザが操作を行うタイミングの節目節目でこのプロトコルで定義されているメソッドをデリゲートに対して送ります。 デリゲートとなるオブジェクトは必要なデータを返したり、アプリの状態をデリゲートメソッドが呼ばれたタイミングに適したものにする処理を行うことが出来ます。 iOSプログラミングでは、クラスの継承によってクラスをカスタマイズさせるのではなく、デリゲートパターンを用いてクラスの一部処理を別のクラスに委譲(delegation)させる手法がよく用いられています。 このアプリの場合、ページ表示画面の動作を全体的に制御しているPageViewContollerをUIScrollViewのデリゲートに設定します。 PageViewContollerクラスの定義で、UIScrollViewDelegateをサポートすることを宣言します。

  PageViewContoller.h
  @interface PageViewController : UIViewController {
次に、InterfaceBuilderで、UIScrollViewのdelegateプロパティにPageViewContollerのインスタンスを指定します。これで、UIScrollViewはUIScrollViewDelegateのメソッドをPageViewContollerに送るようになります。 スクロール動作を始めたときにUIScrollViewがズーム表示する対象となるビューをUIScrollViewに教える処理を実装します。

  - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
    return contentView_;
  }

拡大してスクロール操作をしようとしているのに、タップと見なされてページ遷移してしまうのを防ぐため、拡大中はタップの認識を抑制するようにします。 拡大操作の開始タイミングは先ほどのviewForZoomingInScrollView:が送られることで知ることが出来ます。また、拡大操作が終了するタイミングは、scrollViewDidEndZooming:withView:atScale:がというデリゲートメソッドが送られることで取れます。

部分拡大機能

ビューア系のアプリでよくある、2本指のピンチイン、ピンチアウトで拡大・縮小する機能は前節のように比較的簡単に実装できるのですが、部分的に拡大したい、テンポ良くページを閲覧したい場合には使いづらく感じるときもあります。 そこで、画面をタップした後、そのまま長押ししたらタップした場所の周辺部が拡大された画面が表示され、タップした指を離すと消える、という動作にすれば、ページ遷移の動作の邪魔にならず、必要な部分だけを拡大表示出来るのではないかと、実装してみました。 今回、iOS3.2つまりiPadのリリースに合わせてサポートされたUIPopoverControllerを使って、部分拡大の表示を試してみます。 ポップオーバーとはどんなUI要素かというと、百聞は一見にしかず、下記のスクリーンショットをご覧ください。


このようにマンガの吹き出しのような画面を任意の場所に表示させることができます。指定した大きさのポップオーバーが画面に収まるよう表示位置を自動で調整してくれるのでお手軽です。 UIPopoverControllerを使うには、ポップオーバーの中身として表示するためのビューコントローラを用意する必要があります。 ビューア画面本体と同様、UIScrollViewのズーム機能を使います。 このビューコントローラはInterfaceBuilderを使うほど複雑な階層関係を持たないので、nibは作成せず、loadViewメソッドの呼び出しの中で、プログラマブルにビュー構造を生成します。 ビューの階層関係は、ビューア本体の表示画面同様、UIScrollViewの下位ビューにイメージを表示するビューを配置する形となります。




="prettyprint"> 1 - (void)loadView { 2 3 // ポップオーバー表示時に表示するサイズを指定 4 self.contentSizeForViewInPopover = CGSizeMake( kLupeViewWidth, kLupeViewHeight ); 5 // トップレベルのビューを作る 6 self.view = [[UIView alloc] initWithFrame:CGRectMake(0,0,kLupeViewWidth, kLupeViewHeight)]; 7 8 // スクロールビューを作る 9 lupeScrollView_ = [[UIScrollView alloc] initWithFrame:CGRectMake(0,0,kLupeViewWidth, kLupeViewHeight)]; 10 [self.view addSubview:lupeScrollView_]; 11 12 // イメージ表示ビューを作る 13 lupeImageView_ = [[UIImageView alloc] initWithFrame:contentFrame_]; 14 15 // イメージの縦横比率を保持して、長辺がビューに収まるよう表示する指定 16 lupeImageView_.contentMode=UIViewContentModeScaleAspectFit; 17 18 [lupeScrollView_ addSubview:lupeImageView_]; 19 lupeImageView_.backgroundColor = [UIColor blackColor]; 20 21 22 lupeScrollView_.delegate =self; 23 24 // ズーム可能にする(デフォルト値が1.0なので注意) 25 lupeScrollView_.maximumZoomScale = kLupeZoomRatio; 26 [lupeScrollView_ setZoomScale:kLupeZoomRatio animated:NO]; 27 28 }
上記コードで重要な点としては、このビューコントローラ自身のcontentSizeForViewInPopoverプロパティに適切な値を設定すること(4行目)、あらかじめ設定しておいたcontentFrame_をイメージビューの大きさとして設定する(13行目)でしょう。 実際にポップオーバーで表示させるには、ビューコントローラのviewDidLoadメソッドでUIPopoverControllerを作成し、長押しのハンドラで表示させます。

     1	- (void)viewDidLoad {
     2	  (略)
     3	  // 長押しrecognizerの登録
     4	  UILongPressGestureRecognizer* longTapRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongTapFrom:)];
     5	  [self.view addGestureRecognizer:longTapRecognizer];
     6	  [longTapRecognizer release];
     7
     8	  lupeViewController_ = [[LupeViewController alloc] initWithContentFrame:self.view.frame];
     9	  popover_ = [[UIPopoverController alloc]
    10		       initWithContentViewController:lupeViewController_];
    11
    12	}
UILongPressGestureRecognizerを生成してセットし、長押しが認識されたらhandleLongTapFrom:が呼び出されるように設定します(4-5行目)。次いで、今回作成したLupeViewController をinitWithContentFrame:で初期化することで、ズーム対象となるページ表示画面のサイズをLupeViewControllerに設定しておきます。 UIPopoverControllerにLupeViewControllerのインスタンスを指定して生成すれば準備完了です。

     1	- (void)handleLongTapFrom:(UIGestureRecognizer *)recognizer {
     2	  CGPoint location = [recognizer locationInView:self.view];
     3	  switch (recognizer.state) {
     4	  case UIGestureRecognizerStatePossible:
     5	    break;
     6	  case UIGestureRecognizerStateBegan:
     7	      lupeViewController_.image = [UIImage imageWithCGImage:  (CGImageRef)[[contentView_ layer] contents]];
     8	  case UIGestureRecognizerStateChanged:
     9	    {
    10	      [popover_.contentViewController zoomAt:location];
    11	      [popover_ presentPopoverFromRect:CGRectMake(location.x, location.y, 0,0)
    12			inView:self.view
    13			permittedArrowDirections:UIPopoverArrowDirectionAny  animated:YES];
    14
    15	    }
    16	    break;
    17	  case UIGestureRecognizerStateEnded:
    18	  case UIGestureRecognizerStateCancelled:
    19	    [popover_ dismissPopoverAnimated:YES];
    20	    break;
    21	  }
    22	}
長押しタップのハンドラも、基本的には通常タップのハンドラと同様です。長押しが開始されたタイミングでそのとき表示しているページのイメージをLupeViewControllerのインスタンスに渡し、UIPopoverControllerのpresentPopoverFromRectメソッドを呼び出せばポップオーバーが表示されます。presentPopoverFromRectにはタップされたビュー上の位置を渡します。permittedArrowDirections:にはUIPopoverArrowDirectionAnyを指定することで、ポップオーバーの吹き出しの部分が、ポップオーバーのウインドウ位置とタップされた座標の位置関係にあった形で表示されます。(下図参照)


まとめ

今回ご紹介したUIScrollViewは、UITableViewとならんでiPhoneのスムースな使い勝手に貢献している大きな要素ではないでしょうか。UIScrollViewがサポートする、ハードウェアによるスクロール/拡大機能を活用しない手はありません。本連載では今後も応用例をご紹介していくと思います。 今回のソースは、下記bzrコマンドで取得することができます。

bzr branch -r article-4-release lp:~klabrd/jisuireader/trunk article-4-release

次回予告

しおり機能など、これからやりたいことを実現しようとすると、書籍の内容に加えて、付加的なデータを保存する機能を追加する必要が出てきました。そこで、次回は、Core Dataを用いてデータの永続化を実装してみたいと思います。

前回までの目次





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