左右にスライド操作するとスクロールしてページを切り替える実装をしたいときがあります。nimbusのNIPagingScrollViewを使うと簡単ですので、それを使った実装方法を説明したいと思います。

UIScrollViewの問題

NIPagingScrollViewの説明の前にUIScrollViewで実装する場合の問題点を説明しておきます。

  • ページ数がたくさんあるときはメモリ効率を考えた実装をする必要がある
  • たとえば100ページあって、100ページ分のViewをUIScrollViewに貼り付けるとメモリを圧迫しかねない
  • 現在表示中のページと前後のページくらいをViewとして持つ方式にすると良い
  • これを自分で実装するのは面倒!

NIPagingScrollViewは、この辺を考慮した実装になっています。

サンプルコード

サンプルコードを用いて説明しますので、以下をダウンロードして下さい。

https://github.com/stack3/NimbusSamples

ビルドするためにはgitでsubmoduleを取り込む必要があります。詳しくはREADMEを読んでください。
起動したらNIPagingScrollViewのText and Image Pagesを選択してください。スライドするとテキストページや画像ページに切り替わります。UIPageControlで下端に現在のページ位置が表示されるようにしています。
01-1

02-1

NISPagingScrollViewの配置

本サンプルでは、Auto Layoutを有効にしたxib上にNIPagingScrollViewを配置しています。STPagingScrollViewSampleViewController.xibを開いてください。ちょっとわかりづらいですが、このように配置されています。

05-1

Auto LayoutのConstraintは以下のように設定しています。

  • NIPagingScrollViewはSuperview左右と上端とくっつき、下端と固定の空白をあける
  • UIPageControlは水方向中央配置、Superview左右と下端にくっつく

NIPagingScrollViewを配置する際の注意

Interface Builderで配置するとき、Viewを選択して配置し、Custom ClassをNIPagingScrollViewに変更しましょう。

07

08

ScrollViewを選択して配置してはいけません。NIPagingScrollViewはUIViewを継承しているからです。

次に注意すべきなのはAuto LayoutでNIPagingScrollViewを配置するとき、NIPagingScrollView#translatesAutoresizingMaskIntoConstraintsをYESに設定することです。NIPagingScrollViewはAutoresizingMaskを使ってSubviewの配置を行なっています。よって、そのままでは挙動がおかしくなります。

STPagingScrollViewSampleViewController#init

ViewControllerのinitメソッドを見てみます。

_pageContents = [NSMutableArray arrayWithCapacity:3];        
[_pageContents addObject:@"伊賀上野城は、上野盆地のほぼ中央にある上野台地の北部にある標高184メートルほどの丘に建てられた平山城である。北には服部川と柘植川、南には久米川、西側には木津川の本流が流れ、城と城下町を取り巻く要害の地にある。n引用: http://ja.wikipedia.org/wiki/%E4%B8%8A%E9%87%8E%E5%9F%8E"];
[_pageContents addObject:[UIImage imageNamed:@"castle01.jpg"]];
[_pageContents addObject:[UIImage imageNamed:@"castle02.jpg"]];
[_pageContents addObject:[UIImage imageNamed:@"castle03.jpg"]];
[_pageContents addObject:@"昭和42年(1967年)旧城域一帯が国の史跡に指定されている。城を含めた近隣一帯は上野公園として整備されており、松尾芭蕉を祀る俳聖殿や芭蕉翁記念館があるほか、伊賀流忍者博物館があり、伊賀上野の観光地として利用され、各種イベントなどが行われている。n引用: http://ja.wikipedia.org/wiki/%E4%B8%8A%E9%87%8E%E5%9F%8E"];

配列_pageContentsに表示したいUIImageもしくはNSStringを入れています。

STPagingScrollViewSampleViewController#viewDidLoad

viewDidLoadでNIPagingScrollView/UIPageControlのプロパティ設定などを行なっています。

_pagingScrollView.dataSource = self;
_pagingScrollView.delegate = self;
_pagingScrollView.pageMargin = 0;
_pagingScrollView.translatesAutoresizingMaskIntoConstraints = YES;
[_pagingScrollView reloadData];

NIPagingScrollViewはUITableViewと似ていて、NIPagingScrollViewDataSourceを通じて表示するデータに関する情報を返します。またNIPagingScrollViewDelegateでイベントを受け取ります。最後にreloadDataを呼ぶことでDataSourceメソッドを通じて表示処理が行われます。

pageMarginプロパティですが、デフォルトのままだと以下のようにスクロールの途中でマージンができます。

09

これが嫌な場合はpageMarginを0にすると、マージンをなくすことができます。

10

ちなみにNIPagingScrollView#backgroundColorが白になっているのでマージンも白になっています。黒にする対処でもよさそうです。

次はUIPageControlの初期化について。

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

UIPageControlにページの数とUIPageControlをタップすることでページインデックスが変わった時のイベントを設定しておきます。

NIPagingScrollViewDataSource

numberOfPagesInPagingScrollView:で、ページ数を返すようにします。

- (NSInteger)numberOfPagesInPagingScrollView:(NIPagingScrollView *)pagingScrollView
{
    return _pageContents.count;
}

pagingScrollView:pageViewForIndex:で、ページインデックスに対応するViewを返すようにします。

- (UIView *)pagingScrollView:(NIPagingScrollView *)pagingScrollView pageViewForIndex:(NSInteger)pageIndex
{
    NSObject *pageContent = [_pageContents objectAtIndex:pageIndex];

    NSString *reusePageIdentifier = nil;
    if ([pageContent isKindOfClass:[UIImage class]]) {
        reusePageIdentifier = @"imagePage";
    } else { // pageContent is NSString
        reusePageIdentifier = @"labelPage";
    }

    UIView *page = [_pagingScrollView dequeueReusablePageWithIdentifier:reusePageIdentifier];

    if ([pageContent isKindOfClass:[UIImage class]]) {
        STPagingScrollViewImagePage *imagePage = nil;
        if (page == nil) {
            imagePage = [[STPagingScrollViewImagePage alloc] initWithFrame:pagingScrollView.bounds];
            imagePage.reuseIdentifier = reusePageIdentifier;
            page = imagePage;
        } else {
            imagePage = (STPagingScrollViewImagePage *)page;
        }
        imagePage.imageView.image = (UIImage *)pageContent;
    } else { // pageContent is NSString
        STPagingScrollViewLabelPage *labelPage = nil;
        if (page == nil) {
            labelPage = [[STPagingScrollViewLabelPage alloc] initWithFrame:pagingScrollView.bounds];
            labelPage.reuseIdentifier = reusePageIdentifier;
            page = labelPage;
        } else {
            labelPage = (STPagingScrollViewLabelPage *)page;
        }
        labelPage.label.text = (NSString *)pageContent;
    }

    return page;
}

ちょっと処理が長いですが、このような処理になっています。

  • _pageContentsからpageIndexのオブジェクト(pageContent)を取り出す
  • pageContentの型を判別してreuseIdentifierを決定する
  • reuseIdentifierを指定して、NIPagingScrollView#dequeueReusablePageWithIdentifier:から、使いまわせるページView(page)があれば取得する。なければnil
  • pageContentの型に応じて、pageがnilなら対応するページViewを作りpageへ代入する
    • pageContentがUIImageなら、STPagingScrollViewImagePage
    • pageContentがNSStringなら、STPagingScrollViewLabelPage
  • pageのプロパティにUIImageもしくはNSStringを設定する

基本的にはUITableViewが表示に必要な分だけUITableViewCellを作る仕組みと同じです。PageのViewの型とreuseIdentifierを一対一にすることで、PageのViewを使いまわすようにできます。

NIPagingScrollViewPageプロトコル

ページのViewとなるものはNIPagingScrollViewPageプロトコルを実装する必要があります。本サンプルではSTPagingScrollViewImagePage(UIImage表示用のページ)、STPagingScrollViewLabelPage(NSString表示用のページ)が、このプロトコルを実装しています。

@interface STPagingScrollViewImagePage : UIView<NIPagingScrollViewPage>

@property (nonatomic, readwrite, copy) NSString* reuseIdentifier;
@property (nonatomic, readwrite, assign) NSInteger pageIndex;
@property (strong, nonatomic, readonly) UIImageView *imageView;

@end

reuseIdentifierプロパティはNIPagingScrollViewPageのsuperプロトコルNIRecyclableViewで宣言されていて、プロトコルを実装するクラスでも同様に宣言する必要があります。ここはreuseIdentifierを設定します。

pageIndexプロパティはNIPagingScrollViewPageで宣言されていて、プロトコルを実装するクラスでも同様に宣言する必要があります。ここにはNIPagingScrollViewから現在割り当てられているpageIndexが代入されます。また、STPagingScrollViewImagePageはUIImage表示のためにimageViewをSubviewとしています。

STPagingScrollViewLabelPageはNSString表示のためにNIAttributedLabelをSubviewとしています。

@interface STPagingScrollViewLabelPage : UIView<NIPagingScrollViewPage>

@property (nonatomic, readwrite, copy) NSString* reuseIdentifier;
@property (nonatomic, readwrite, assign) NSInteger pageIndex;
@property (strong, nonatomic, readonly) NIAttributedLabel *label;

@end

NIAttributedLabelもnimbusで用意されているクラスです。URLを自動でリンク表示してくれます。

NIPagingScrollViewDelegate

スライド操作でNIPagingScrollViewのページが切り替わった時のイベントを処理します。

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

NIPagingScrollViewのページが切り替わったら、UIPageControlも合わせて更新する必要があります。NIPagingScrollView#centerPageIndexで変更後のページインデックスを得られます。それをUIPageControlに設定します。

一応、すでに同じページなら更新しないようにしておくとよさそうです。UIPageControlのイベント経由でプログラムでNIPagingScrollView#centerPageIndexを設定したときも、このdelegateメソッドが呼ばれるからです。

UIPageControlのUIControlEventValueChangedイベント

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

UIPageControlをタップした時、UIPageControl上でページインデックスが切り替わります。これに合わせてNIPagingScrollViewのページインデックスも更新する必要があります。先ほどと逆の代入処置をしています。

NIPagingScrollViewのまとめ

UITableViewのようにDataSource、Delegateの実装が若干面倒ですが、自分で実装するよりは楽なはずです。なかなか良い設計だと思います。

最後に要点をまとめておきます。

  • NIPagingScrollViewのページViewとなるものは、NIPagingScrollViewPageプロトコルを実装する
  • NIPagingScrollViewを表示するViewController、もしくはViewは、NIPagingScrollViewDataSource、NIPagingScrollViewDelegateを実装する
  • NIPagingScrollViewDataSource#numberOfPagesInPagingScrollView:で全体のページ数を返す
  • NIPagingScrollViewDataSource#pagingScrollView:pageViewForIndex:でページViewを返す。
  • ページViewはNIPagingScrollView#dequeueReusablePageWithIdentifierを生成済みなら使って使いまわすようにする
  • NIPagingScrollViewDelegate#pagingScrollViewDidChangePages:で、ページが変わった時の処理をする