iOSのページングの仕組みを紐解いてフロー図にしてみた


iOSのUIPageViewControllerは思ったより有能だったという話です。
ただ「どう有能なのか」を理解するのに自分は思ったより時間がかかったので、まとめです。

![](https://i.imgur.com/JgO0V63.gif)

※UIPageViewControllerはこんな感じのもの。

## 前提

- UIScrollViewは今回使ってません。
- ボタン等でPageViewを遷移させる場合は今回の話対象外です。UIPageViewControllerDataSourceのメソッドが呼ばれないので。

## 開発環境

- Xcode 10.1
- Swift 4.2

## 通常のUIPageViewControllerの実装方法

4枚のViewControllerをUIPageViewControllerで切り替えできるように実装する場合、
下記のような感じのコードになると思います。サンプルです。

- `Sample.storyboard` 上に `SamplePageViewController` と4つのViewControllerがある
- 他の画面から `SamplePageViewController` に遷移することを想定してる

```swift

class SamplePageViewController: UIPageViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        dataSource = self
        setViewControllers([getFirst()], direction: .forward, animated: true, completion: nil)
    }

    func getFirst() -> UIViewController {
        let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "FirstViewController")
        return viewController
    }

    func getSecond() -> UIViewController {
        let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "SecondViewController")
        return viewController
    }

    func getThird() -> UIViewController {
        let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "ThirdViewController")
        return viewController
    }

    func getFourth() -> UIViewController {
        let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "FourthViewController")
        return viewController
    }
}

extension SamplePageViewController: UIPageViewControllerDataSource {
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        if viewController.isKind(of: FourthViewController.self) {
            // 今Fourthなら、Before画面はThird
            return getThird()
        } else if viewController.isKind(of: ThirdViewController.self) {
            // 今Thirdなら、Before画面はSecond
            return getSecond()
        } else if viewController.isKind(of: SecondViewController.self) {
            // 今Secondなら、Before画面はFirst
            return getFirst()
        } else {
            return nil
        }
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        if viewController.isKind(of: FirstViewController.self) {
            // 今Firstなら、After画面はSecond
            return getSecond()
        } else if viewController.isKind(of: SecondViewController.self) {
            // 今Secondなら、After画面はThird
            return getThird()
        } else if viewController.isKind(of: ThirdViewController.self) {
            // 今Thirdなら、After画面はFourth
            return getFourth()
        } else {
            return nil
        }
    }
}
```

storyboard使ってるとかView周りの扱い方は人それぞれだと思います、
今回はView周りの設定には言及しません。

`UIPageViewControllerDataSource` で2つのメソッドを利用しています。
以降、呼びやすいように `AfterPage` ・ `BeforePage` と表記します。

**AfterPageとBeforePageを使う場合について！この記事では言及します！**

## AfterPageとBeforePageの問題

AfterPageとBeforePageとは、
上記のコードを見れ貰えれば分かる人は分かると思いますが、
ページを左右にフリックで遷移できるように設定している部分です。

- AfterPage: →側に遷移する際の画面の設定
- BeforePage: ←側に遷移する際の画面の設定

このイベントに合わせて、各々のページに追加アクションさせようとすると、事件が発生します。
例えば、

```swift
func getFirst() -> UIViewController {
    let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
    let viewController = storyBoard.instantiateViewController(withIdentifier: "FirstViewController")
    navigationItem.title = "Firstやで" // <-追加
    return viewController
}
```

このように、遷移に合わせてtitleを変更するように書いてみます。（4画面とも
-> なぜか思うようにtitleが変わってくれない。。。なんで。。。

調べていくと、AfterPageとBeforePageが呼ばれるタイミングに問題がありました！
「フリックする度に1回呼ばれる」は間違っていました！

## フロー図でAfterPage・BeforePageのロードを追う

First->Second->Third->Fourth->Third->Second->Firstで遷移する例を基に、
フロー図でAfterPage・BeforePageのロードを追ってみます。

- 補足1: ロード = AfterPage or BeforePageの呼び出し。
- 補足2: ロードすると、ロードしたViewをPageViewControllerが持つことになります。

![](https://i.imgur.com/f6bXSAs.png)

![](https://i.imgur.com/ZfETAtB.png)

![](https://i.imgur.com/CycASNE.png)

![](https://i.imgur.com/SNWC8GH.png)

![](https://i.imgur.com/45njm4K.png)

![](https://i.imgur.com/0JPz44t.png)

![](https://i.imgur.com/bIRNL6B.png)

![](https://i.imgur.com/7HCSOox.png)

![](https://i.imgur.com/fGTRw8U.png)

![](https://i.imgur.com/4DHWXuj.png)

![](https://i.imgur.com/rqwyfDu.png)

![](https://i.imgur.com/iDymFPe.png)

![](https://i.imgur.com/ilPTbM3.png)

## フロー図から見えた要点

- PageViewControllerは最大、表示Viewと前後の合計3つまでViewを持つ
- 最初は、最初の画面（First）しか持っていない
- フリックをしようとして、遷移先のページを持っていなければ、AfterPage or BeforePageをロードしてページを取得する
- フリックでページングの遷移が完了すると、表示Viewの前後のViewを確認し、なければAfterPage or BeforePageをロードして取得する
- 持っているViewが、表示Viewまたは前後のViewでなければ破棄する

※要点多くなって要点っぽくなくなってしまった_(:3」∠)_

**PageViewControllerが持つViewを3つに自動で設定してくれていて有能！！**
と僕は感じました。おかげでViewを保持容量を制御してくれているので。

## 結論1:遷移のタイミングでアクションを追加したいなら

AfterPage or BeforePageのタイミングに追加するのはオススメしません。
その代わり、UIPageViewControllerDelegateに別の便利メソッドがあります。

```swift
extension PageViewController: UIPageViewControllerDelegate {

    // ページ遷移が完了したら、titleを変更する
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        // viewControllers?[0] は現在のViewControllerを示す
        guard completed, let currentViewController = viewControllers?[0] else {
            return
        }
        switch currentViewController {
        case is FirstViewController:
            navigationItem.title = "First"
        case is SecondViewController:
            navigationItem.title = "Second"
        case is ThirdViewController:
            navigationItem.title = "Third"
        case is FourthViewController:
            navigationItem.title = "Fourth"
        default:
            break
        }
    }
}
```

ページ遷移が完了したタイミングで呼び出されるメソッドを利用すれば、
titleを変更する処理を問題なく実現できました。

## 結論2:AfterPageとBeforePageの内容を最適化するなら

BeforePageの下記コードは不要です。

```swift
if viewController.isKind(of: FourthViewController.self) {
    // 今Fourthなら、Before画面はThird
    return getThird()
}
```

なぜなら、Fourthの時点で、PageViewControllerがThirdを持っていないことはないからです。
そのため、上記コードは省略できます。

## まとめ

省略しないで書くことを勧めます。w
説明をちゃんとしないと省略していい理由が分からないですし。
可読性を考えると、省略しないことを勧めます。

PageViewControllerの有能な点を理解して、活かした実装にしましょう。

## 補足：たまにPageView完了時にAfterPage呼び出されない

First -> Secondに遷移した際に、Thirdを読み込まないケースが自分の場合ありました。
フリックの勢いのせいかなwくらいしか検討がつかなくて、
原因はあんまり良くわかっていないので、知っている人いたら教えてくださいm(__)m

## 参考サイト

https://swiswiswift.com/2018/06/21/page-view-controller-with-page-control/
https://qiita.com/Takeshi_Akutsu/items/dbf54df8e8a50e8ed5be
https://qiita.com/yajamon/items/e1754e7fc847b595c26a