12
votes

I've got a tableView inside of a pageViewController and when swiping on a cell to bring up the option to delete the cell the gesture is only recognized under certain circumstances, say you swiped very quickly and aggressively.

I imagine this is happening because it's not sure whether the swiping gesture is meant for the pageView or the tableView. Is there a way to specifically determine where the swipe gesture is happening to enable a nice smooth display of the delete button?

5
You could add an edit button. Then when in edit mode you could disable the page controller swipe gesture (by setting the delegate nil).user1199624

5 Answers

14
votes

Theory:

Both UIPageViewController and UITableView are implemented using UIScrollView, where UIPageViewController embeds UIScrollView and UITableView is a subclass of UIScrollView

UITableView also uses a couple of UIPanGestureRecognizers to bring in all the magic. One of these is UISwipeActionPanGestureRecognizer which handles the swipe to delete actions.

This issue is caused because UIPageViewControllers UIPanGestureRecognizer wins in the conflict with the UITableViews UISwipeActionPanGestureRecognizers.

So we have to some how tell UIPageViewController to ignore gestures if UITableViews UISwipeActionPanGestureRecognizer are in action.

Luckily there is something already provided by UIGestureRecognizer.

UIGestureRecognizer's require(toFail otherGestureRecognizer: UIGestureRecognizer) creates a relation between the two gesture recognizers that will prevent the gesture's actions being called until the other gesture recognizer fails.

So all we had to do is fail UIPageViewControllers embedded UIScrollviews panGestureRecognizer when UITableViews UISwipeActionPanGestureRecognizer are triggered.

There are two ways you could achieve this.

Solution 1: Add a new gesture recognizer to table view and Mimic UISwipeActionPanGestureRecognizer. And make UIPageViewController panGesture require to fail this new gestureRecognizer

Solution 2 (A bit dirty): Make a string comparision to the UITableView's UISwipeActionPanGestureRecognizer and make UIPageViewController panGesture require to fail this new gestureRecognizer

Code:

Solution 1 A helpful utility to get UIPageViewControllers embedded UIScrollView

extension UIPageViewController {

    var scrollView: UIScrollView? {
        return view.subviews.first { $0 is UIScrollView } as? UIScrollView
    }

}

Add the below code to UIViewController holding the UITableView and call it from viewDidLoad()

func handleSwipeDelete() {
    if let pageController = parent?.parent as? UIPageViewController {
        let gestureRecognizer = UIPanGestureRecognizer(target: self, action: nil)
        gestureRecognizer.delaysTouchesBegan = true
        gestureRecognizer.cancelsTouchesInView = false
        gestureRecognizer.delegate = self
        tableView.addGestureRecognizer(gestureRecognizer)

        pageController.scrollView?.canCancelContentTouches = false
        pageController.scrollView?.panGestureRecognizer.require(toFail: gestureRecognizer)
    }
}

And finally delegate methods

func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
        return false
    }

    let translation = panGesture.translation(in: tableView)
    // In my case I have only trailing actions, so I used below condition.
    return translation.x < 0
}

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                       shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return otherGestureRecognizer.view == tableView
}

Solution 2 (A bit dirty) A helpful utility to get UIPageViewControllers embedded UIScrollView

extension UIPageViewController {

    var scrollView: UIScrollView? {
        return view.subviews.first { $0 is UIScrollView } as? UIScrollView
    }

}

Add the below code to UIViewController holding the UITableView and call it from viewDidLoad()

func handleSwipeDelete() {
    guard let pageController = parent as? UIPageViewController else {
        return
    }

    pageController.scrollView?.canCancelContentTouches = false
    tableView.gestureRecognizers?.forEach { recognizer in
        let name = String(describing: type(of: recognizer))
        guard name == "_UISwipeActionPanGestureRecognizer" else {
            return
        }
        pageController.scrollView?
            .panGestureRecognizer
            .require(toFail: recognizer)
    }
}
6
votes

I had the same problem. I found a solution that works well.

Put this in your UIPageViewController's viewDidLoad func.

if let myView = view?.subviews.first as? UIScrollView {
    myView.canCancelContentTouches = false
}

PageViewControllers have an auto-generated subview that handles the gestures. We can prevent these subviews from cancelling content touches. The tableview will be able to capture swipes for the delete button, while still interpreting swipes that fail the tableview's gesture requirements as page swipes. The delete button will show in cases where you hold and swipe or swipe "aggressively."

0
votes

You can set delaysContentTouches to false on the tableView itself as well. This solution worked for my collection view's UISlider elements.

See Swift 4.0 code below:

yourTableView.delaysContentTouches = false
0
votes

I have found a working solution by reassigning UIScrollView's panGestureRecognizer's delegate to my class and ignoring the original delegate when a right-to-left pan is detected. I used method swizzling for that.

Please check my sample project: https://github.com/kambala-decapitator/SwipeToDeleteInsidePageVC

0
votes

In case you have a tableView in the first or the last viewController inside of your UIPageViewController maybe think of disabling the bouncing at the end of your UIPageViewController first and then implement this solution.

In case you don't know how to disable the bounce of the UIPageViewController, check out my answer here.