hyoromoのブログ

最近はVRSNS向けに作ったものについて書いています

UIPageViewControllerの使い方 -基礎-


iOS電子書籍のようなUIを作りたい場合、UIPageViewControllerという素晴らしいUIが存在します。これを使えば、ページ移動時に「ページをペラッとめくってる」や「横へスクロール」するようなアニメーションが簡単に実装できちゃいます。とは言うものの、UIPageViewControllerは構造が少し複雑で、それを把握せずに実装しちゃうと意味不明な詰まり方をします。
そこで基礎編/Tips編と分けて自分なりに整理して書き残します。今回は基礎編なので、ざっくりとした考え方と使い方の説明となります。

UIPageViewControllerで何が出来るか

何が出来るのかについて列挙します。

  • 複数の画面を1画面上に配置し、スワイプする事でページを閲覧する
  • ページをめくるようなアニメーション
  • ページを左右へスクロールするアニメーション(iOS6以降((iOS5でも行いたい場合はUIScrollViewで別途実装する必要があります)))
  • ページ移動方法は、右開き(左へスワイプしてページ移動)と左開き(右へスワイプしてページ移動)
  • 任意ページへの移動
  • 見開きページ(2ページ合わせて表示)

テンプレートを眺めてみる

UIPageViewControllerを知るにはAppleが用意しているテンプレートのコードを追う方法が一番手っ取り早いです。
Xcodeの新規プロジェクト作成から「Page-Baseed Application」を選択して、まずはテンプレートを元にしたプロジェクトを作成してください。

テンプレートのソースファイル構成

作成されたソースファイルを確認してみてください。Xcode ver 4.6.1では以下のように作成されます。

  • RootViewController
    UIPageViewControllerを扱うViewControllerです。ここでインスタンス生成しているので、設定等はここに書かれています。
  • ModelController
    ページを管理するクラスです。各ページを表示する時にページ毎にDataViewControllerがインスタンス生成され、ページに対してデータを送りたい場合はここに定義する事になります。他にはページの「進む/戻る」を制御してたりもします。
  • DataViewController
    表示するページです。表示させたいページ内容に応じ、ここに定義します。

図にすると以下のような関係になってます。


UIPageViewControllerのインスタンス生成と初期化

MainStoryboard_iPhone.storyboard と RootViewController.m を参照ください。storyboard上に設置されたUIPageViewControllerをRootViewController.mのviewDidLoadメソッド内で以下のような初期設定が行われています。インラインで説明すると書きにくかったのでコード上にコメントを書いてます。

// インスタンス生成
self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil]; // 指定内容に関しては次回説明します
// UIPageViewControllerDelegateプロトコル
self.pageViewController.delegate = self; // ページめくり開始/終了などのdelegate methodがあるのでコントロールしたい場合は設定しとく
// 表示するページを設定
DataViewController *startingViewController = [self.modelController viewControllerAtIndex:0 storyboard:self.storyboard]; // storyboard上で定義しているページ画面
NSArray *viewControllers = @[startingViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL];
// 管理クラスを設定
 self.pageViewController.dataSource = self.modelController;
// addChildViewControllerした後にする必要があるオマジナイ的なヤツ
[self.pageViewController didMoveToParentViewController:self];
// ジェスチャー設定
self.view.gestureRecognizers = self.pageViewController.gestureRecognizers; // UIPageViewControllerが行うジェスチャーをInterceptする事もできる
ページをめくり

ModelController.m を参照ください。先ほどと同じようにコードにコメント書いて説明します。

- (id)init
{
    self = [super init];
    if (self) {
        // ページに表示するデータの初期化。サンプルアプリが月名をページに表示してるので、ここは月データを扱ってるというだけ
        // RootViewController.mでModelControllerのインスタンス生成時にページデータを渡し、ここでページデータを保持する方法を取る
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        _pageData = [[dateFormatter monthSymbols] copy];
    }
    return self;
}
// ページのインスタンスを生成するメソッド。ページ移動した時などに使われる
- (DataViewController *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard
{
    // indexの値がページ情報の件数と整合性が取れてるか確認してるだけ
    if (([self.pageData count] == 0) || (index >= [self.pageData count])) {
        return nil;
    }
    
    // ページのインスタンスを生成。ここでinitで保持しておいたページ情報を受け渡す
    // これでDataViewControllerの内容がUIPageViewController上に表示される
    DataViewController *dataViewController = [storyboard instantiateViewControllerWithIdentifier:@"DataViewController"];
    dataViewController.dataObject = self.pageData[index];
    return dataViewController;
}
// privateなメソッドで、引数のvewControllerがどのページかを返してるだけ
- (NSUInteger)indexOfViewController:(DataViewController *)viewController
{
    return [self.pageData indexOfObject:viewController.dataObject];
}
// UIPageViewController上で右スワイプした時に呼ばれるメソッド
// 勘違いしやすいのは名前にbeforeと付いているからといって必ずしも前ページへの移動の時だけに呼ばれるという訳ではないです
// 左開きの時は戻る場合、右開きの時は進む場合に呼ばれます
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
    // ページの整合性をチェック
    // 左開きか右開きかでここの処理は変わり、以下のコードは左開きの場合の処理です
    NSInteger page = [self indexOfViewController:(MangaDataViewController *)viewController];
    if (page == NSNotFound) {
        return nil;
    }
    if (page == 0) {
        return nil;
    }
    page--;
    
    // 前述したようにページのインスタンス生成を行う
    return [self viewControllerAtIndex:page storyboard:viewController.storyboard];
}
// UIPageViewController上で左スワイプした時に呼ばれるメソッド
// 全てにおいて先ほどのメソッドと逆になります
// 左開きの時は進む場合、右開きの時は戻る場合に呼ばれます。
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
    NSInteger page = [self indexOfViewController:(MangaDataViewController *)viewController];
    if (page == NSNotFound) {
        return nil;
    }
    if (page == [self.pageData count] + 1) {
        return nil;
    }
    page++;
    
    return [self viewControllerAtIndex:page storyboard:viewController.storyboard];
}
ページ表示

ページ(DataViewControllerクラス)はModelControllerにより、ページの数分だけインスタンス生成される事はここまでの説明で分かったかと思います。あとは受け渡されたデータを元に好きにページを表示するだけで良いです。
インスタンスは2ページ移動すると破棄されます。

まとめ

サンプルを眺めたお陰でUIPageViewControllerの構成が理解出来た気になれたかと思います。
次のTips編ではサンプルプロジェクトをベースに電子書籍アプリでやりたいであろう機能をTips形式で列挙していく予定です。

追記(2013/04/22)

Tips編を書きました。