iOSアプリでよく見かける紙芝居式のオンボーディング画面を UIPageViewController で作るメモです。もっともシンプルな例だと思うのに、実装に 不満ポイント がたくさんなのでそれもふまえて。
作りたいもの
- アプリの説明が書かれた数枚のページをスワイプ操作で進める(戻る)
- 現在のページ位置は画面下部のページインジケータのドットでわかる
つまり、こんなやつです。色使いが気持ち悪いですが、ビューの構成をわかりやすくするためなので我慢してください 。
作り方
準備
UIPageViewController
を継承したクラスを作り、紙芝居の各ページとなる UIViewController
を用意します。ここではページが変わっていることがわかるように背景が異なるだけのページを3枚用意し、最初は1枚目を表示するようにします。
class PageViewController: UIPageViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 紙芝居1ページ目を設定します
self.setViewControllers([pages[0]], direction: .forward, animated: true, completion: nil)
}
/// 紙芝居の各ページとなる UIViewController
private lazy var pages: [UIViewController] = {
return [createPage(color: .red), createPage(color: .blue), createPage(color: .green)]
}()
private func createPage(color: UIColor) -> UIViewController {
let page = UIViewController()
page.view.backgroundColor = color
return page
}
}
ここが不満!
setViewControllers
メソッドは複数の UIViewController
を設定できるので、3ページ分突っ込めば良さそうに見えるのに、実際には表示する UIViewController
のみを設定する。
ページの切り替え
ページの切り替えを実装するためには UIPageViewControllerDataSource
プロトコルを実装します。実装必須のメソッドが2つあり、このメソッドの引数で指定されたページの前後にくるページを返すようにします。
class PageViewController: UIPageViewController, UIPageViewControllerDataSource {
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.setViewControllers([pages[0]], direction: .forward, animated: true, completion: nil)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let pageIndex = pages.firstIndex(of: viewController), pageIndex - 1 >= 0 {
return pages[pageIndex - 1]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let pageIndex = pages.firstIndex(of: viewController), pageIndex + 1 < pages.count {
return pages[pageIndex + 1]
}
return nil
}
}
ここが不満!
メソッドの引数で指定された UIViewController
から前後のページを判断するのは面倒。ここでは用意した pages
からインデックスを求めていて回りくどい。
現在のページ位置の表示
ここまでの実装では画面下部にあるページインジケータの表示は行われません。これを表示させるためにはドキュメントにはこう書かれています。
// A page indicator will be visible if both methods are implemented, transition style is 'UIPageViewControllerTransitionStyleScroll', and navigation orientation is 'UIPageViewControllerNavigationOrientationHorizontal'.
// Both methods are called in response to a 'setViewControllers:...' call, but the presentation index is updated automatically in the case of gesture-driven navigation.
まず UIPageViewController
をインスタンス化するときに次のようにトランジションスタイルとナビゲーションの向きを指定してやります。
let pageViewController = PageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
そして先ほどの UIPageViewControllerDataSource
プロトコルにあるオプショナルな2つのメソッドを実装します。これらのメソッドは上記にあるとおり setViewControllers
が呼ばれたときに呼び出されます。なので presentationIndex(for:)
の方は1ページ目となる0を指定しています。
class PageViewController: UIPageViewController, UIPageViewControllerDataSource {
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return pages.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
return 0
}
}
ここが不満!
スワイプ操作に加えて、ページを進めるボタンを用意するみたいなケースもよくありますが、そのボタンタップ時に改めて setViewController
を呼ぶ必要があり、このあたりの実装が厄介。例えばページ1からページ2に進めるならこうです。
setViewControllers([pages[1]], direction: .forward, animated: true, completion: nil)
この場合、現在のページを把握して次のページに移動する必要がありますが、こんなコードになります。。。
if let currentPage = viewControllers.first, let currentPageIndex = pages.first(of: currentPage), currentPageIndex+1 < pages.count {
setViewControllers([pages[currentPageIndex+1]], direction: .forward, animated: true, completion: nil)
}
さらに先ほど書いたとおり setViewControllers
を実行すると presentationIndex(for:)
で適切なインデックスを返さなければなりません。これはこんなコードになってしまいます。。。
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
if let currentPage = viewControllers.first, let currentPageIndex = pages.first(of: currentPage) {
return currentPageIndex
}
return 0
}
ページインジケータの色を変える
ページインジケータは UIPageControl というコンポーネントですが、画面の背景色やアプリのカラースキームによって色を変えたくなります。これは UIPageControll
の下記のプロパティで変更することができます。
-
backgroundColor
(.yellow
) -
pageIndicatorTintColor
(.lightGray
) -
currentPageIndicatorTintColor
(.purple
)
()内は最初の例のGIFアニメで設定している色なので、どこの色が変わるのか確認できます。
しかしながら、UIPageViewController
が表示する UIPageControl
には アクセスできません(不満爆発)。
設定するには UIPageViewController
のビューの階層をたどって UIPageControl
を見つけて色を設定するか、UIPageControl.appearance()
でデフォルトの色設定を変えてやるしかありません。今回は後者で実装しました。ふぅ。。。
class PageViewController: UIPageViewController, UIPageViewControllerDataSource {
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.setViewControllers([pages[0]], direction: .forward, animated: true, completion: nil)
UIPageControl.appearance().backgroundColor = .yellow
UIPageControl.appearance().pageIndicatorTintColor = .lightGray
UIPageControl.appearance().currentPageIndicatorTintColor = .purple
}
}
全コード
今回の全コードをまとめたのがこちらです。Xcode 11.2 の Playground で確認しています。
import UIKit
import PlaygroundSupport
class PageViewController: UIPageViewController, UIPageViewControllerDataSource {
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.setViewControllers([pages[0]], direction: .forward, animated: true, completion: nil)
UIPageControl.appearance().backgroundColor = .yellow
UIPageControl.appearance().pageIndicatorTintColor = .lightGray
UIPageControl.appearance().currentPageIndicatorTintColor = .purple
}
// MARK: - UIPageViewControllerDataSource
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let pageIndex = pages.firstIndex(of: viewController), pageIndex - 1 >= 0 {
return pages[pageIndex - 1]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let pageIndex = pages.firstIndex(of: viewController), pageIndex + 1 < pages.count {
return pages[pageIndex + 1]
}
return nil
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return pages.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
return 0
}
// MARK: - Private
/// 紙芝居の各ページとなる UIViewController
private lazy var pages: [UIViewController] = {
return [createPage(color: .red), createPage(color: .blue), createPage(color: .green)]
}()
private func createPage(color: UIColor) -> UIViewController {
let page = UIViewController()
page.view.backgroundColor = color
return page
}
}
PlaygroundPage.current.liveView = PageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)