はじめに
下記のような、UIPageViewController内のTableViewCellのスワイプ削除を実現したいという要件に遭遇することがありました。
- UIPageViewController
- 左のページ:通常のViewController
- 右のページ:TableViewController
UIPageViewController内では左右のスワイプでページ切り替えを行います。ページ切り替えの方向的に右のページに実装されたtableViewのスワイプ削除は共存させられそうですが、実際に実装してみるとPageViewControllerのGestureが優先され、下のgifのように削除ができなくなってしまします。(なぜかシミュレーターでは削除できるのですが実機で起動すると添付のような動きになります)
今回は独自でUIPanGestureRecognizerをハンドリングすることで、スワイプ方向にあわせてページ切り替えとTableViewのセル削除の優先度を制御するようにしました。
方針
pageViewController内の右ページのtableViewにおいて以下の方針でスワイプ方向に応じて優先するgestureを決定します。
判定は主にスワイプを始めた位置のセルのUITableViewCell.EditingStyle
とスワイプ方向(右か左か)を用いて以下のように決めます。
-
UITableViewCell.EditingStyle
が.delete
- いかなる場合でもtableViewの制御を優先
-
UITableViewCell.EditingStyle
が.delete
以外- 左 <- 右
- tableViewのスワイプ削除を優先
- 左 -> 右
- pageViewControllerのページ切り替えを優先
- 左 <- 右
UIGestureRecognizer周りの調整
細かい実装はstack overflowを参考に実装しました
本事象はUIPageViewControllerのview.subViews内のUIScrollViewのUIPanGestureRecognizerと、UITableViewが継承しているUIScrollViewのUIPanGestureRecognizerが干渉することで生じています。
そこで2つのUIPanGestureRecognizerの間の優先度をハンドリングするべく、UIGestureRecognizerのrequire(toFail otherGestureRecognizer: UIGestureRecognizer)
メソッドを使用します。
このメソッドでは指定されたotherGestureRecognizerに対して、自身のGestureの優先度を下げることができます。(厳密にはotherGestureRecognizerがUIGestureRecognizerStateRecognized
かUIGestureRecognizerStateBegan
になったときに指定したGestureを失敗させることができます)
これを使ってtableViewのUIPanGestureRecognizerのスワイプが評価されたときに、スワイプ方向に応じてUIPageViewControllerのGestureを失敗させることで採用すべきUIGestureRecognizerを決定します。
大まかな実装のイメージとしては以下のようになります。
①tableViewを内包するVCにUIPanGestureRecognizerを加える
↓
②tableViewのGestureが認識されたときにpageViewControllerのGestureより優先的に評価するように設定
↓
③UIGestureRecognizerDelegateのデリゲートメソッドでスワイプ方向によってtableViewのGestureを認識するか制御
(認識した場合、pageViewControllerのGestureよりtableViewのGestureが優先される)
ソース
final class PageViewController: UIPageViewController {
var scrollView: UIScrollView? {
if let v = self.view as? UIScrollView {
return v
}
for v in view.subviews where v is UIScrollView {
return v as? UIScrollView
}
return nil
}
...
}
import UIKit
final class ChildTableViewController: UIViewController {
@IBOutlet private weak var tableView: UITableView! {
didSet {
self.handleTableViewDeletableGesture()
}
}
private var deletableGestureRecognizer: UIPanGestureRecognizer?
private var data = [Int](1...20)
override func viewDidLoad() {
super.viewDidLoad()
}
}
// MARK: - UITableViewDataSource
extension ChildTableViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! ChildTableViewCell
cell.setData("No. \(String(self.data[indexPath.row]))")
return cell
}
}
// MARK: - Setup DeletableGesture
extension ChildTableViewController {
/// TableViewに優先的に判定されるPanGestureを設定する
private func handleTableViewDeletableGesture() {
// 初期化
self.initializeGesture()
let pageController = self.parent as! PageViewController
// ①tableViewを内包するVCにUIPanGestureRecognizerを加える
self.deletableGestureRecognizer = UIPanGestureRecognizer(target: self, action: nil)
self.deletableGestureRecognizer?.delaysTouchesBegan = true
self.deletableGestureRecognizer?.cancelsTouchesInView = false
self.deletableGestureRecognizer?.delegate = self
self.tableView.addGestureRecognizer(self.deletableGestureRecognizer!)
pageController.scrollView?.canCancelContentTouches = false
// ②tableViewのGestureが認識されたときにpageViewControllerのGestureより優先的に評価するように設定
pageController.scrollView?.panGestureRecognizer.require(toFail: self.deletableGestureRecognizer!)
}
/// セル削除用のTapGestureの初期化
private func initializeGesture() {
guard let gesture = self.deletableGestureRecognizer,
let pageController = self.parent as? PageViewController else {
return
}
self.tableView.removeGestureRecognizer(gesture)
self.deletableGestureRecognizer = nil
pageController.scrollView?.canCancelContentTouches = true
}
}
// MARK: - UITableViewDelegate
extension ChildTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
self.data.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
}
// MARK: - UIGestureRecognizerDelegate
extension ChildTableViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard
let panGesture = gestureRecognizer as? UIPanGestureRecognizer,
let indexPath = self.tableView.indexPathForRow(at: panGesture.location(in: self.tableView)),
let cell = self.tableView.cellForRow(at: indexPath) else {
return false
}
// panGestureの移動量
let translation = panGesture.translation(in: self.tableView)
// スワイプを始めたセルの編集モードで分岐
switch cell.editingStyle {
case .delete:
return true // 削除表示されているセルは常にtableViewの制御を優先
case .none, .insert:
return translation.x < 0 // その他のセルは【左 <- 右】の時のみtableViewの制御(削除)を優先
@unknown default:
return false
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return otherGestureRecognizer.view == self.tableView
}
}