LoginSignup
7
8

More than 3 years have passed since last update.

UIPageViewController内でUITableViewのスワイプ削除を実装する

Posted at

はじめに

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

  • UIPageViewController
    • 左のページ:通常のViewController
    • 右のページ:TableViewController

after.gif

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

before.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がUIGestureRecognizerStateRecognizedUIGestureRecognizerStateBeganになったときに指定したGestureを失敗させることができます)

これを使ってtableViewのUIPanGestureRecognizerのスワイプが評価されたときに、スワイプ方向に応じてUIPageViewControllerのGestureを失敗させることで採用すべきUIGestureRecognizerを決定します。

大まかな実装のイメージとしては以下のようになります。

①tableViewを内包するVCにUIPanGestureRecognizerを加える

②tableViewのGestureが認識されたときにpageViewControllerのGestureより優先的に評価するように設定

③UIGestureRecognizerDelegateのデリゲートメソッドでスワイプ方向によってtableViewのGestureを認識するか制御
(認識した場合、pageViewControllerのGestureよりtableViewのGestureが優先される)


ソース

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
    }

    ...
}    
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
    }
}

プロジェクト

参考

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8