Nimbusの中に複数の画像をページ表示するのに便利なクラスがあります。今回はそのNIPhotoAlbumScrollViewクラスの説明です。

NIPhotoAlbumScrollViewの機能

  • 複数の画像をページ単位で表示
  • 最初はサムネイルを表示し、サーバーからオリジナルの画像をダウンロードし終わったら、それを表示する
  • 画像をズーム

以前に書いたNIPagingScrollViewと似ているのですが、画像だけを扱うならNIPhotoAlbumScrollViewを使ったほうが手軽かもしれません。またNIPagingScrollViewを読んでから、この記事を読んだほうが理解が早いかもしれません。NIPhotoAlbumScrollViewはNIPagingScrollViewを継承していますので。

サンプルコードをGitHubに用意したのでダウンロードして下さい。

https://github.com/stack3/NimbusSamples

ビルドするためにはsubmoduleを取り込む必要があります。READMEに従ってください。

サンプル

サンプルを起動したら、NIPhotoAlbumScrollView/Photo Albumを選択してください。

01-1

このように左右にスライドさせてページを切り替えると画像も切り替わるようになっています。

ピンチ操作もしくはダブルタップで画像をズームさせることもできます。

04

見ただけではわかりづらいのですが、最初はサムネイルを表示して、サーバーからオリジナル画像を読み終わったら、それを表示しています。

STPhotoAlbumScrollViewSampleViewController.xib

STPhotoAlbumScrollViewSampleViewController.xibで、NIPhotoAlbumScrollViewとUIPageControlをこのように配置しています。

05-1

NIPhotoAlbumScrollViewを配置する際に注意するのはViewとして配置してからCustom ClassをNIPhotoAlbumScrollViewに変更することです。

07

06

Auto Layoutで配置していますが、Constraintは以下のようになっています。

  • NIPhotoAlbumScrollView: Superviewとの上左右間隔0px、下端は36px固定。つまり高さは端末サイズに合わせて可変。
  • UIPageControl: Superviewとの左右下間隔0px。つまりtop座標は端末サイズに合わせて可変。

STPhotoAlbumScrollViewSampleViewController.h

NIPhotoAlbumScrollViewを使うために、NimbusPhotos.hをimportします。

#import "NimbusPhotos.h"

NIPhotoAlbumScrollViewのDataSourceとDelegateを実装するので、そのように指定します。

@interface STPhotoAlbumScrollViewSampleViewController : UIViewController<NIPhotoAlbumScrollViewDataSource, NIPhotoAlbumScrollViewDelegate>

STPhotoAlbumPhotoInfo.h

STPhotoAlbumPhotoInfoクラスに表示する画像の1つ1つの情報を格納します。

@interface STPhotoAlbumPhotoInfo : NSObject

// サムネイル画像
@property (strong, nonatomic) UIImage *thumbnailImage;
// オリジナル画像のサーバー上のURL
@property (strong, nonatomic) NSURL *originalImageURL;
// オリジナル画像のサイズ
@property (nonatomic) CGSize originalImageSize;
// オリジナル画像。ダウンロードするまではnil
@property (strong, nonatomic) UIImage *originalImage;
// オリジナル画像の状態
@property (nonatomic) STPhotoAlbumPhotoInfoOriginalImageState originalImageState;

@end

STPhotoAlbumPhotoInfoOriginalImageStateは、オリジナル画像がサーバーから読込中か、読み込まれたかどうかを判定するためのenum値です。

typedef enum {
    STPhotoAlbumPhotoInfoOriginalImageStateNone = 0, // 未読み込み
    STPhotoAlbumPhotoInfoOriginalImageStateLoading,  // 読込中
    STPhotoAlbumPhotoInfoOriginalImageStateLoaded    // 読み込み済み
} STPhotoAlbumPhotoInfoOriginalImageState;

STPhotoAlbumScrollViewSampleViewController.m

以下の様なメンバ変数を持ちます。

// NIPhotoAlbumScrollView
IBOutlet __weak NIPhotoAlbumScrollView *_photoAlbumScrollView;
// UIPageControl
IBOutlet __weak UIPageControl *_pageControl;
// STPhotoAlbumPhotoInfoの配列
__strong NSMutableArray *_photos;
// オリジナル画像をダウンロードするNSOperationを処理するためのキュー
__strong NSOperationQueue *_operationQueue;

init

initメソッドで_photosを初期化します。

_photos = [NSMutableArray arrayWithCapacity:3];

STPhotoAlbumPhotoInfo *photoInfo;
photoInfo = [[STPhotoAlbumPhotoInfo alloc] init];
photoInfo.thumbnailImage = [UIImage imageNamed:@"castle01.jpg"];
photoInfo.originalImageURL = [NSURL URLWithString:@"http://cdn-ak.f.st-hatena.com/images/fotolife/e/eimei23/20130518/20130518091536.jpg?1368836295"];
photoInfo.originalImageSize = CGSizeMake(800, 600);
[_photos addObject:photoInfo];

// 残り2つも同様に追加

viewDidLoad

Auto Layoutを使う場合、NIPhotoAlbumScrollView#translatesAutoresizingMaskIntoConstraintsをYESにしないと正常に動作しないので注意してください。

_photoAlbumScrollView.translatesAutoresizingMaskIntoConstraints = YES;

画像のズームを可能にします。これをYESにしてもオリジナル画像が読まれるまではズームはできないことに注意してください。

_photoAlbumScrollView.zoomingIsEnabled = YES;

ViewControllerがDataSourceとDelegateを実装するのでselfを渡します。

_photoAlbumScrollView.dataSource = self;
_photoAlbumScrollView.delegate = self;

reloadDataでDataSourceのメソッドが呼ばれて、画像表示の処理が行われるようになります。

[_photoAlbumScrollView reloadData];

UIPageControlの方もページ数をあわせておきます。またタップされてページインデックスが変わった時のイベントも受け取るようにします。

_pageControl.numberOfPages = _photos.count;
[_pageControl addTarget:self action:@selector(pageControlDidChangeValue) forControlEvents:UIControlEventValueChanged];

回転時の対応

そのままだと回転した時にNIPhotoAlbumScrollViewのレイアウトが崩れるので、以下のように回転時の対処が必要です。

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    [_photoAlbumScrollView willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
}

- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    [_photoAlbumScrollView willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
}

NIPhotoAlbumScrollViewDataSource

NIPhotoAlbumScrollViewDataSourceはNIPagingScrollViewDataSourceを継承していて、そのメソッドも一部実装する必要があります。ちなみにNIPagingScrollViewで引数に渡ってくるものは、実体はNIPhotoAlbumScrollView自身です。

必要なページ数を返します。

- (NSInteger)numberOfPagesInPagingScrollView:(NIPagingScrollView *)photoAlbumScrollView
{
    return _photos.count;
}

各ページを表示するためのViewを返します。今回はNIPhotoAlbumScrollViewで用意されているものを使うので、以下のようにしています。

- (UIView *)pagingScrollView:(NIPagingScrollView *)photoAlbumScrollView pageViewForIndex:(NSInteger)pageIndex
{
    return [_photoAlbumScrollView pagingScrollView:photoAlbumScrollView pageViewForIndex:pageIndex];
}

カスタマイズしたければここでカスタムViewを返します。その場合、NIPagingScrollViewの再利用の仕組みを使うことになります。それに関しては、NIPagingScrollViewの記事を参考にしてください。

次はNIPhotoAlbumScrollViewDataSourceのメソッドです。ここで表示するUIImageを返します。

- (UIImage *)photoAlbumScrollView:(NIPhotoAlbumScrollView *)photoAlbumScrollView
                     photoAtIndex:(NSInteger)photoIndex
                        photoSize:(NIPhotoScrollViewPhotoSize *)photoSize
                        isLoading:(BOOL *)isLoading
          originalPhotoDimensions:(CGSize *)originalPhotoDimensions
{
    STPhotoAlbumPhotoInfo *photoInfo = [_photos objectAtIndex:photoIndex];

    *originalPhotoDimensions = photoInfo.originalImageSize;
    UIImage *image = nil;
    if (photoInfo.originalImageState != STPhotoAlbumPhotoInfoOriginalImageStateLoaded) {
        *isLoading = YES;
        *photoSize = NIPhotoScrollViewPhotoSizeThumbnail;
        image = photoInfo.thumbnailImage;

        if (photoInfo.originalImageState == STPhotoAlbumPhotoInfoOriginalImageStateNone) {
            photoInfo.originalImageState = STPhotoAlbumPhotoInfoOriginalImageStateLoading;

            NSURLRequest *request = [NSURLRequest requestWithURL:photoInfo.originalImageURL];
            AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request success:^(UIImage *image) {
                photoInfo.originalImage = image;
                photoInfo.originalImageState = STPhotoAlbumPhotoInfoOriginalImageStateLoaded;
                [_photoAlbumScrollView didLoadPhoto:image atIndex:photoIndex photoSize:NIPhotoScrollViewPhotoSizeOriginal];
            }];
            [_operationQueue addOperation:operation];
        }
    } else {
        *isLoading = NO;
        *photoSize = NIPhotoScrollViewPhotoSizeOriginal;
        image = photoInfo.originalImage;
    }

    return image;
}

長いコードですが、以下の様な処理をしています。

  • 引数photoIndexから処理すべきSTPhotoAlbumPhotoInfo(photoInfo)を得る
  • *originalPhotoDimensionsにオリジナル画像のサイズを代入する
  • オリジナル画像が読み込み済みでない場合
    • *isLoadingをYESとする。オリジナル画像を読込中だからである
    • *photoSizeにNIPhotoScrollViewPhotoSizeThumbnail(サムネイル)を代入する
    • サムネイル画像を戻り値に返すようにする
    • オリジナル画像が未読み込みならAFImageRequestOperationを使って読む
    • 読み込んだら、NIPhotoAlbumScrollView#didLoadPhotoを呼んで、オリジナル画像が読み終わったことを伝える
  • オリジナル画像が読み込み済みの場合
    • *isLoadingをNOとする。オリジナル画像を読込済だからである
    • *photoSizeにNIPhotoScrollViewPhotoSizeOriginal(オリジナル)を代入する
    • オリジナル画像を返す

肝心なのは画像は戻り値で返して、その他の必要な情報は引数のポインタを介して返すようになっていることです。C言語やC++の経験がなくObjective-Cから入った人には、ちょっとわかりづらいかもしれません。

実践ではオリジナル画像をファイルキャッシュする仕組みが必要かもしれません。その場合は、SDWebImageと組み合わせると良いのではと思います。

NIPhotoAlbumScrollViewDelegate

NIPhotoAlbumScrollViewをスライド操作してページが切り替わった時、UIPageControl#currentPageを設定して同期します。

- (void)pagingScrollViewDidChangePages:(NIPagingScrollView *)photoAlbumScrollView
{
    if (_pageControl.currentPage != _photoAlbumScrollView.centerPageIndex) {
        _pageControl.currentPage = _photoAlbumScrollView.centerPageIndex;
    }
}

UIPageControl

UIPageControlをタップしてページが切り替わった時、NIPhotoAlbumScrollView#centerPageIndexを設定して同期します。

- (void)pageControlDidChangeValue
{
    if (_photoAlbumScrollView.centerPageIndex != _pageControl.currentPage) {
        _photoAlbumScrollView.centerPageIndex = _pageControl.currentPage;
    }
}

以上で終わりです。ちょっと長いコードですが、自分で全て実装するよりは楽なはずです。