iOS で UISegmentedControl ライクでリッチな上タブを実現するライブラリはいくつかありますが、Swift 5 にも対応していて最も有名なもののひとつ、XLPagerTabStrip。
このライブラリを使ってページを切り替えたときのイベントを処理する際に困ったので、備忘録として残しておきます。
前提として、ButtonBarPagerTabStripViewController
のサブクラスで実装するとします。
問題: moveToViewController(at:animated:)
はスワイプ時に呼ばれない
ページを切り替える操作は、以下の2パターンがあると思います。
1. ページラベルを直接タップする
2. スワイプで隣のページに移動する
上記のうち、2. スワイプで隣のページに移動する とき、moveToViewController(at:animated:)
が呼ばれません。
したがって、このメソッドの中でページ切り替え時の処理を書くと、ラベルを直接タップしてページ切り替えをしたときにした期待した動作をしないと思います。
import XLPagerTabStrip
class TabPageViewController : ButtonBarPagerTabStripViewController {
// スワイプ切替の場合には呼ばれないため、ページ切替のイベントハンドラーとしては使用しないべき
open override func moveToViewController(at index: Int, animated: Bool = true) {
super.moveToViewController(at: index, animated: animated)
}
}
解決策: updateIndicator(for:fromIndex:toIndex:withProgressPercentage:indexWasChanged:)
を使う
ただし、少し工夫する必要があります。
そもそもこのメソッドは、ページのインジケータの遷移状況を知らせてくれるものなので、ページの移動が開始されてから完了するまで、数十回にわたって発火します。
以下は渡ってくる引数の説明です。
-
viewContoller: PagerTabStripViewController
- 親となっている ViewController(このメソッドをオーバーライドしている自分自身)
-
fromIndex: Int
- 移動元となるページ番号(過渡状態の値が入ってくるので注意!)
-
toIndex: Int
- 移動先となるページ番号
-
progressPercentage: CGFloat
- 遷移の進捗率(1.0 = 完了)
-
indexWasChanged
- 飛び番指定でページ移動を行うときに true が入ってくる
このメソッドの特性を踏まえて、前述の2パターンのページ切り替えのイベントを過不足なく拾い上げるための処理を、以下のように実装しました。
/// ページラベルをタップして切り替える際に飛び番を指定したときに、
/// 過渡状態のイベントをスキップするためのフラグ
///
/// 例)ページ1から4に直接移動するとき、1->2, 2->3, 3->4 が順に呼ばれるが、
/// 実質的に処理したいのは 1->4 であるため、途中を省く必要がある
private var skipChangeIndexJumping: Bool = false
/// 上記 skipChangeIndexJumping の説明にあるように、途中のインデックスを省くために
/// 実際の切り替え元のインデックスを憶えておくための一次変数
private var actualFromIndex: Int = 0
open override func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool) {
super.updateIndicator(for: viewController, fromIndex: fromIndex, toIndex: toIndex, withProgressPercentage: progressPercentage, indexWasChanged: indexWasChanged)
// 遷移中は省く
guard progressPercentage == 1.0 else { return }
// 跳び番を指定すると indexWasChanged == true で入ってくるので、これを飛び番移動が起きた判定とする
if indexWasChanged {
skipChangeIndexJumping = true
}
// 飛び番移動したとき、最後に from, to, current が揃うので、そのタイミングを遷移完了とする
if fromIndex == toIndex, toIndex == currentIndex {
skipChangeIndexJumping = false
}
guard !skipChangeIndexJumping else { return }
/*
** ここにページ切替後に行いたい処理を書く
*/
actualFromIndex = fromIndex
}
※ ちなみに、余計なイベントを無視する処理は、RxSwift で sentMessage や methodInvoked を使ってこのメソッドの呼び出しを拾って debounce で間引くという手もあったかもしれませんが、今回はこちらの方法を使いました