The cleanest way is to subclass your navigation controller and add a directional pan gesture recognizer to its view that borrows its target/action properties from the default interaction pan gesture recognizer.
First, create a directional pan gesture recognizer that simply puts itself into a failed state if the initial gesture is not in the desired direction.
class DirectionalPanGestureRecognizer: UIPanGestureRecognizer {
enum Direction {
case up
case down
case left
case right
}
private var firstTouch: CGPoint?
var direction: Direction
init(direction: Direction, target: Any? = nil, action: Selector? = nil) {
self.direction = direction
super.init(target: target, action: action)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
firstTouch = touches.first?.location(in: view)
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
switch state {
case .possible:
if let firstTouch = firstTouch,
let thisTouch = touches.first?.location(in: view) {
let deltaX = thisTouch.x - firstTouch.x
let deltaY = thisTouch.y - firstTouch.y
switch direction {
case .up:
if abs(deltaY) > abs(deltaX),
deltaY < 0 {
break
} else {
state = .failed
}
case .down:
if abs(deltaY) > abs(deltaX),
deltaY > 0 {
break
} else {
state = .failed
}
case .left:
if abs(deltaX) > abs(deltaY),
deltaX < 0 {
break
} else {
state = .failed
}
case .right:
if abs(deltaX) > abs(deltaY),
deltaX > 0 {
break
} else {
state = .failed
}
}
}
default:
break
}
super.touchesMoved(touches, with: event)
}
override func reset() {
firstTouch = nil
super.reset()
}
}
Then subclass UINavigationController
and perform all of the logic in there.
class CustomNavigationController: UINavigationController {
let popGestureRecognizer = DirectionalPanGestureRecognizer(direction: .right)
override func viewDidLoad() {
super.viewDidLoad()
replaceInteractivePopGestureRecognizer()
}
private func replaceInteractivePopGestureRecognizer() {
guard let targets = interactivePopGestureRecognizer?.value(forKey: "targets") else {
return
}
popGestureRecognizer.setValue(targets, forKey: "targets")
popGestureRecognizer.delegate = self
view.addGestureRecognizer(popGestureRecognizer)
interactivePopGestureRecognizer?.isEnabled = false // this is optional; it just disables the default recognizer
}
}
And then conform to the delegate. We only need the first method, gestureRecognizerShouldBegin
. The other two methods are optional.
Most apps that have this feature enabled won't work if the user is in a scroll view and it's still scrolling; the scroll view must come to a complete stop before the swipe-to-pop gesture is recognized. This is not how it works with the default recognizer so the last two methods of this delegate (1) allow simultaneous gesturing with scroll views but (2) force the pop recognizer to fail when competing with the scroll view.
// MARK: - Gesture recognizer delegate
extension CustomNavigationController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer.view is UIScrollView {
return true
}
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer.view is UIScrollView {
return true
}
return false
}
}