📘

UIPageViewControllerの左右のキャッシュが消せなかった

2023/11/20に公開

UIPageViewController を使うと、左右にスワイプして画面遷移するViewが簡単につくれます。
pageCurl を指定すると、ページめくりアニメーションがつくんですけど、これ求めてる開発者どのぐらいいるんでしょうね)

困ったこと

今回、ログイン時は2ページ、ログアウトすると1ページというViewに、UIPageViewController を適用しました。
ログイン状況によってページ数を変えたかったんですが、これが上手くいきませんでした。

ViewControllerのキャッシュ

UIPageViewController が保持する左右のViewControllerは、下記のDelegate経由で指定します。

extension ViewController: UIPageViewControllerDataSource, UIPageViewControllerDelegate {
    var viewControllers: [UIViewController]
    func pageViewController(_: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        if let index = viewControllers.firstIndex(of: viewController),
           index > 0
        {
            return viewControllers[index - 1]
        } else {
            return nil
        }
    }

    func pageViewController(_: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        if let index = viewControllers.firstIndex(of: viewController),
           index < viewControllers.count - 1
        {
            return viewControllers[index + 1]
        } else {
            return nil
        }
    }
}

これで、viewControllers を更新したらOKだと思っていたんですが、ダメでした。

インディケーター

ページ数を指定したらいいのか?と思って、下記のDelegateに必死でページ数渡してもみました。

func presentationCount(for pageViewController: UIPageViewController) -> Int

これも意味ありませんでした。
よくよく調べたら、ページインディケーターのためのDelegateでした。

func presentationCount(for pageViewController: UIPageViewController) -> Int
func presentationIndex(for: UIPageViewController) -> Int

↑この2メソッドをセットで実装すると、ページ下部にドットで「・・・」みたいなページインディケーターが表示されます。

キャッシュを消す正式な手段がない

どうやら、UIPageViewController は scroll 指定だと、左右のViewControllerをキャッシュする仕様があるみたいです。
setViewControllers(_:direction:animated:completion:)をしても、キャッシュが消えないようです。
そもそもこのメソッド、viewControllers を配列で指定するのではなく、1ページしか渡せません。
(設定によって何渡すかが変わるらしい)

今回僕が実装したかった要件だと、一度2ページで表示してしまうと、その後1ページ設定に変更したいというのができませんでした。

ワークアラウンド

対策としては、UIPageViewController のインスタンスをつくりなおすとか、2ページはUIPageViewControllerで1ページのときは使わないとかが考えられます。
ただどちらにせよちょっと扱いがめんどくさいですよね。

今回の要件だと、こんな方法がありました。

https://stackoverflow.com/questions/15325891/refresh-uipageviewcontroller-reorder-pages-and-add-new-pages

こちらのstackoverflowで紹介されているワークアラウンドです。

pageViewController.dataSource = nil

この指定をすると、左右スワイプが無効になります。
その後また参照をセットしなおすと確かに更新されるんですが、この更新に関しては僕が試した限りだと不安定で、上手く制御できませんでした。

公式ドキュメント見ると、

https://developer.apple.com/documentation/uikit/uipageviewcontroller/1614117-datasource

If the value of this property is nil, then gesture-based navigation is disabled.

という記述があって、左右スワイプ無効は保証されていました。
今回の要件だと、1ページor2ページだったので、1ページのときはdataSourceをnilにする、という実装にしました。

今後のための考察

UIPageViewController は、動的にページ数変えることは可能なんですけど、
それをやる場合、左右のページをキャッシュする仕様を意識してやらないと意図せぬバグを仕込んじゃいそうだなと思いました。
というか、何らかの更新手段を提供して欲しいですね。

(了)

Discussion