UIPageViewController内でUITableViewのスワイプ削除を実装する

## はじめに
下記のような、UIPageViewController内のTableViewCellのスワイプ削除を実現したいという要件に遭遇することがありました。

- UIPageViewController
    - 左のページ：通常のViewController
    - 右のページ：TableViewController

![after.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287671/87dcf862-a9db-8868-69b6-da6d41252a79.gif)



UIPageViewController内では左右のスワイプでページ切り替えを行います。ページ切り替えの方向的に右のページに実装されたtableViewのスワイプ削除は共存させられそうですが、実際に実装してみるとPageViewControllerのGestureが優先され、下のgifのように削除ができなくなってしまします。（なぜかシミュレーターでは削除できるのですが実機で起動すると添付のような動きになります）

![before.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287671/77545c71-20e6-97dd-64b4-a796752d8522.gif)


今回は独自でUIPanGestureRecognizerをハンドリングすることで、スワイプ方向にあわせてページ切り替えとTableViewのセル削除の優先度を制御するようにしました。

## 方針

pageViewController内の右ページのtableViewにおいて以下の方針でスワイプ方向に応じて優先するgestureを決定します。
判定は主にスワイプを始めた位置のセルの`UITableViewCell.EditingStyle`とスワイプ方向（右か左か）を用いて以下のように決めます。

- `UITableViewCell.EditingStyle` が `.delete`
    - いかなる場合でもtableViewの制御を優先
- `UITableViewCell.EditingStyle` が `.delete`以外
    - 左 <- 右
        - tableViewのスワイプ削除を優先
    - 左 -> 右
        - pageViewControllerのページ切り替えを優先

## UIGestureRecognizer周りの調整

細かい実装は[stack overflow](https://stackoverflow.com/questions/36686048/swipe-to-delete-on-a-tableview-that-is-inside-a-pageviewcontroller/38927196)を参考に実装しました

本事象は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が優先される)


---

## ソース

```swift:PageViewController.swift
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
    }

    ...
}    
```

```swift:ChildTableViewController.swift
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
    }
}

```

## プロジェクト
- [Github - DeletablePageVC](https://github.com/ichikawa7ss/DeletablePageVC)

## 参考
- [stack overflow - Swipe to delete on a tableView that is inside a pageViewController
](https://stackoverflow.com/questions/36686048/swipe-to-delete-on-a-tableview-that-is-inside-a-pageviewcontroller/38927196)
- [よくわかるUIGestureRecognizerDelegate](https://qiita.com/ruwatana/items/16997b1b416512c20fb6)
- [【決定版】UIPageViewControllerの使い方（Swift）](https://qiita.com/Takeshi_Akutsu/items/dbf54df8e8a50e8ed5be)