前回に引き続き、画像ビューワの作り方を解説していこう。今日は、画像を表示してフリックによるスクロールに対応するところまでだ。

ビューコントローラの宣言

この画像ビューワでは、画像の読み込みやフリックへの対応といった処理は、ビューコントローラで行っている。まずは、その定義を紹介しよう。次のような宣言にした。

List 1.

@interface ImageViewController : UIViewController
{
    int _index;

    IBOutlet UIScrollView*  _mainScrollView;

    IBOutlet UIView*        _innerView;
    IBOutlet UIScrollView*  _subScrollView;
    IBOutlet UIImageView*   _imageView0;
    IBOutlet UIImageView*   _imageView1;
    IBOutlet UIImageView*   _imageView2;
}

画像ビューワの内部状態を管理するためのインスタンス変数が、1つだけある。_indexという名前の変数だ。この変数は、現在何枚目の画像を表示しているかを表すものだ。従って、中央に表示する画像は_indexにあたるもの。左の画像は_index-1、右の画像は_index+1ということになる。

あとは、Interface Builderで配置したビューに対するアウトレットだ。

また、Interface Builderでの設定も確認しておこう。まず、UIScrollViewのデリゲートとして、このビューコントローラを接続する。フリック操作の入力は、UIScrollViewのデリゲートメソッドとして受け取るからだ。

さらに、前回説明し忘れたちょっとしたテクニックを紹介しよう。この画像ビューワでは横に3枚画像を並べるのだが、それらがぴったりくっついて配置されていると、見づらい。そこで、それぞれの画像の間に余白を挟んで配置した。だが、何も考えずにこのまま動かすと、UIScrolViewによるページングの位置がずれてしまう。ページングでは、UIScrollViewの大きさにあわせてページの大きさを決めるからだ。では、どうすればいいか?こういうときは、UIScrollViewの大きさを、余白分広げてやればいい。今回の場合は余白を20ピクセルにしたので、メインスクロールビューの大きさを、画面より横に20ピクセル大きくしている。これにより、期待通りのページングが行われることになる。

フリック操作の受付

続いて、実装に入る。まず初期化として、画像3枚用のUIImageViewを持っているビューを、メインスクロールビューに設定する。このとき、スクロールビューのコンテントサイズの設定も行っておこう。さらに、インスタンス変数の初期化も行っておく。

List 2.

- (void)viewDidLoad
{
    // innerViewをメインスクロールビューに追加
    [_mainScrollView addSubview:_innerView];

    // メインスクロールビューのコンテントサイズを設定
    _mainScrollView.contentSize = _innerView.frame.size;

    // インデックスの初期値として-1を設定
    _index = -1;
}

次に、UIScrollViewDelegateメソッドを実装する。今回実装するのは、scrollViewDidEndDragging:willDecelerate:と、scrollViewDidEndDecelerating:の2つだ。これらが呼び出されたことによって、ユーザがフリック操作を行ったと判定しているのだ。

これらの中でやっていることは、ただ一つ。画像の更新を行う_renewImagesメソッドを呼び出すことだ。このメソッドについては、次で説明する。

List 3.

- (void)scrollViewDidEndDragging:(UIScrollView*)scrollView
        willDecelerate:(BOOL)decelerate
{
    // メインスクロールビューの場合
    if (scrollView == _mainScrollView) {
        if (!decelerate) {
            // 画像の更新
            [self _renewImages];
        }
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView
{
    // メインスクロールビューの場合
    if (scrollView == _mainScrollView) {
        // 画像の更新
        [self _renewImages];
    }
}

画像の更新

さて、画像ビューワの処理の中心となるのが、これから説明する_renewImagesメソッドだ。

このメソッドで行う処理は、大きく2つに分かれる。まず、ユーザのフリック操作によって、前の画像に移動したか、次の画像に移動したかを判定する。次に、画像が移動した場合は、3枚の画像の更新処理を行うのだ。

まずは画像移動の処理を行うソースコードを紹介しよう。

List 4.

- (void)_renewImages
{
    CGRect      rect;
    NSString*   fileName;
        NSString*   path;
    UIImage*    image;

    // 現在のインデックスを保存
    int oldIndex = _index;

    // コンテントオフセットを取得
    CGPoint offset;
    offset = _mainScrollView.contentOffset;
    if (offset.x == 0) {
        // 前の画像へ移動
        _index--;
    }
    if (offset.x >= _mainScrollView.contentSize.width - CGRectGetWidth(_mainScrollView.frame)) {
        // 次の画像へ移動
        _index++;
    }

    // インデックスの値をチェック
    if (_index < 0) {
        _index = 0;
    }
    if (_index > _maxPage - 1) {
        _index = _maxPage - 1;
    }

    if (_index == oldIndex) {
        return;
    }

    ...

フリックで移動したかどうかは、メインスクロールビューのコンテントオフセットのx座標の値で判定する。xが0であれば、いちばん左までスクロールされたので、前の画像に移動する。xが右側の画像を表示している値であれば、次の画像に移動するのだ。このようにして、_indexの値を更新している。

画像が移動した場合は、3つの画像の更新処理を行う。長くなってしまうので、左側の画像の更新を行うところだけ紹介しよう。

List 5.

    ...

    //
    // 左側のimage viewを更新
    //

    // 最初の画像のとき
    if (_index == 0) {
        // 左側のimage viewは表示しない
        rect = CGRectZero;

        image = nil;
    }
    // 最初の画像以外のとき
    else {
        // 左側のimage viewのframe
        rect.origin = CGPointZero;
        rect.size = self.view.frame.size;

        // 左側のimage viewの画像の読み込み
        fileName = [NSString stringWithFormat:@"%03ds", _index - 1];
        path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"jpg"];
        image = [[UIImage alloc] initWithContentsOfFile:path];
    }

    // 左側のimage viewの設定
    _imageView0.frame = rect;
    _imageView0.image = image;
    [image release];

    ...

_indexの値をもとに、画像とそれを表示するためのフレームの大きさを決定している。_indexが0であれば、左には画像を表示しない。それ以外の場合は、_index-1の画像を表示するのだ。画像の読み込みは、今回は画像のファイル名を「001s.jpg」という感じにしたので、数値を当てはめてファイル名を決定し読み込んでいる。

このような処理を、真ん中と右の画像に対しても行ってやる。

最後に、メインスクロールビューの更新を行う。コンテントサイズとコンテントオフセットを更新してやるのだ。

List 6.

    ...

    //
    // メインスクロールビューの更新
    //

    // コンテントサイズとオフセットの設定
    CGSize  size;
    size.width = CGRectGetMaxX(_imageView2.frame) > 0 ?
            CGRectGetMaxX(_imageView2.frame) + 20.0f :
            CGRectGetMaxX(_subScrollView.frame) + 20.0f;
    size.height = 0;
    _mainScrollView.contentSize = size;
    _mainScrollView.contentOffset = _subScrollView.frame.origin;
}

コンテントサイズは、いちばん最初または最後の画像を表示しているときは画像2枚分、それ以外のときは画像3枚分となる。コンテントオフセットは、サブスクロールビューが表示されるところにあわせてやればいい。

これで、フリックによる画像ビューワは出来上がりだ。次回は、ピンチイン/アウトによる拡大について議論しよう。

ここまでのソースコード: Image-1.zip