UIPageViewController
が登場したのが古いこともあり、公式ドキュメントや、ネットに出ているの情報だと、いまいち各メソッドの挙動までは掴みづらかったので、改めてまとめてみました。
- UIPageViewControllerの使い所
- UIPageViewController.setViewControllers
- UIPageViewControllerDataSource
- UIPageViewControllerDelegate
- 実装例
UIPageViewControllerの使い所
電子書籍リーダーのようなスタンダードな使い方から、アプリのインストール時のガイド、タブUIなど、スワイプでView Controllerを切り替えたい際に使えます。
写真のプレビューなど、中のViewだけ切り替えたい時には、UISwipeGestureRecognizer
などを使うという方法もあるので、
要素の数や内容によってはUIPageViewController
適さない場面もあります。
UIPageViewController.setViewControllers
UIPageViewController
のsetViewControllers
はviewDidLoad
の時とページジャンプなどスワイプ以外で表示するView Controllerを変更したい際に利用します。
第一引数の[UIViewController]
の要素数は、表示するページの枚数になっている必要があります。
UIPageViewControllerDataSource
DataSource
の実装は必須となっていて、そのページの前後のView Controllerを返す2つのメソッドを定義する必要があります。
pageViewController(_:viewControllerBefore:)
pageViewController(_:viewControllerAfter:)
このメソッドが呼ばれるタイミングは、スワイプが完了してView Controllerが切り替わった後と、setViewControllers
などのあとのスワイプ開始時になります。
タイミングが二つある理由は、UIPageViewController
はおそらくアニメーションのために、表示するページの前後のView Controllerを事前に知る必要があるためです。
スワイプ完了時
こちらはスワイプした方向の次のView Controllerを取得します。
例えば、1ページ目から2ページ目にスワイプした場合、pageViewController(_:viewControllerAfter:)
の方が呼ばれ、3ページ目にあたるView Controllerを取得しにいきます。
ただし、こちらはこのタイミングで呼ばれず、下記のスワイプ開始時のタイミングで呼ばれることもあります。(条件特定できず...)
スワイプ開始時
ページジャンプなど、setViewControllers
した直後や先ほどタイミングで呼ばれなかった場合は、前後のView Controllerがわからない状態なので、スワイプ開始時に、pageViewController(_:viewControllerBefore:)
とpageViewController(_:viewControllerAfter:)
の両方を呼びます。
スワイプが完了時のタイミングでも呼ばれるので、これらのメソッドはBeforeとAfter合わせて、計3回呼ばれることになります。
注意点
これらを踏まえると、こちらでページ切り替わりのイベントを発行したり、indexをインクリメント、デクリメントするのはDataSource
の意図するところではないため、あまり良くない気がします。
UIPageViewControllerDelegate
先ほどのページ切り替わりのイベントやindexの変更を行いたい場合は、こちらで行うことになります。
pageViewController(_:willTransitionTo:)
pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:)
transitionの最初と最後にこれらのメソッドが呼ばれるので、こちらを利用します。
切り替わりのイベントを発行する例
例えば1ページのみ表示する場合で、そのページのindexを引数としてイベントを発行する場合、このようになります。
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { // toIndexの部分はindexに変換する適当な処理 guard let index = pendingViewControllers.first?.toIndex() else { return } nextViewControllerIndex = index } func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { if completed { // 適当なイベント viewModel.changeSelectedPageIndex(to: nextViewControllerIndex) } }
スワイプし始めて、遷移せず戻る場合などもあるので、completed
で遷移したかを確認します。
また、pageViewController.viewControllers
などでも現在のView Controllerがとれるので、
開始時に特にすることがなければ、終了時のメソッドだけでもOKです。
実装例
実際のコードから引っ張っているので、若干説明不足なところもあると思います。
コードからView ControllerをinstantiateViewController
で毎回生成するケースです。
PageViewController
enum
にStoryboard Identifierをまとめています。
final class SamplePageViewController: UIPageViewController { enum ViewControllerIdentifier: String, CaseIterable { case dummy1 = "Dummy1VC" case dummy2 = "Dummy2VC" case dummy3 = "Dummy3VC" static func index(of value: String) -> Int? { return ColorPickerPageViewController.ViewControllerIdentifier(rawValue: value) .flatMap(ViewControllerIdentifier.allCases.firstIndex) } func instantiate() -> UIViewController? { let storyboard = UIStoryboard(name: "Main", bundle: nil) return storyboard.instantiateViewController(withIdentifier: self.rawValue) } } override func viewDidLoad() { super.viewDidLoad() dataSource = self delegate = self setViewControllers( [ViewControllerIdentifier.dummy1.instantiate()].compactMap({ $0 }), direction: .forward, animated: true, completion: nil ) } }
UIPageViewControllerDelegate
先ほどとほぼ同じです。
extension SamplePageViewController: UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { guard let index = pendingViewControllers.first?.restorationIdentifier .flatMap(ViewControllerIdentifier.index) else { return } nextViewControllerIndex = index } func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { if completed { viewModel.selectedPageIndex = nextViewControllerIndex } } }
UIPageViewControllerDataSource
extension SamplePageViewController: UIPageViewControllerDataSource { private enum NeighborDirection { case previous, next } func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { return getNeighborViewController(for: viewController, direction: .previous) } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { return getNeighborViewController(for: viewController, direction: .next) } private func getNeighborViewController(for viewController: UIViewController, direction: NeighborDirection) -> UIViewController? { guard let index = viewController.restorationIdentifier .flatMap(ViewControllerIdentifier.index) else { return nil } switch direction { case .previous: guard index > 0 else { return nil } return ViewControllerIdentifier.allCases[index - 1].instantiate() case .next: guard index < ViewControllerIdentifier.allCases.count - 1 else { return nil } return ViewControllerIdentifier.allCases[index + 1].instantiate() } } }