UINavigationController的pop右滑手势失效的解决办法

1,574 阅读2分钟

解决pop手势失效的方法

系统的UINavigationController有一个pop手势,只要手指在屏幕左边滑动,当前的视图控制器的视图会随着手指移动,当手指离开屏幕时,系统会自动判断是否执行pop操作。

但是自定义左边按钮或隐藏了导航栏后,手势会失效。

要解决手势失效的问题,要先研究pop手势。

@available(iOS 7.0, *)
open var interactivePopGestureRecognizer: UIGestureRecognizer? { get }

pop手势是在iOS7.0的时候添加的,是UIGestureRecognizer?类型的只读属性。

先打印一下看看:

<UIScreenEdgePanGestureRecognizer: 0x7ff22ff0ea30; state = Possible;
delaysTouchesBegan = YES; view = <UILayoutContainerView
0x7ff22ff0d810>; target= <(action=handleNavigationTransition:,
target=<_UINavigationInteractiveTransition 0x7ff22ff0e8f0>)>>

再打印一下pop手势的delegate:

<_UINavigationInteractiveTransition: 0x7ff22ff0e8f0>

可以发现pop手势的target和delegate都是<_UINavigationInteractiveTransition: 0x7ff22ff0e8f0>,_UINavigationInteractiveTransition是一个私有类,看不到头文件,不过可以通过

@available(iOS 2.0, *)
public func class_copyMethodList(_ cls: AnyClass?, _ outCount: UnsafeMutablePointer<UInt32>?) -> UnsafeMutablePointer<Method>?

获取到类的方法列表。如下所示:

.cxx_destruct
gestureRecognizer:shouldReceiveTouch:
gestureRecognizerShouldBegin:
gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:
startInteractiveTransition
screenEdgePanGestureRecognizer
_gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:
initWithViewController:animator:
_setShouldReverseLayoutDirection:
setNotInteractiveTransition
gestureRecognizerView
_configureNavigationGesture

可以发现_UINavigationInteractiveTransition实现了UIGestureRecognizerDelegate的三个方法,这几个方法是控制pop手势有效和失效的关键。

先写一个类观察一下_UINavigationInteractiveTransition的行为。

open class NavigationInteractiveTransition: NSObject {
    public private(set) weak var navigationController: UINavigationController?
    public let wrapped: UIGestureRecognizerDelegate?
    public init(_ navigationController: UINavigationController) {
        self.navigationController = navigationController
        self.wrapped = navigationController.interactivePopGestureRecognizer?.delegate
    }
}
extension NavigationInteractiveTransition: UIGestureRecognizerDelegate {
    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if let result = wrapped?.gestureRecognizerShouldBegin?(gestureRecognizer) {
            print("begin", result)
            return result
        }
        return true
    }
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        if let result = wrapped?.gestureRecognizer?(gestureRecognizer, shouldReceive: touch) {
            print("touch", result)
            return result
        }
        return true
    }
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if let result = wrapped?.gestureRecognizer?(gestureRecognizer, shouldRecognizeSimultaneouslyWith: otherGestureRecognizer) {
            print("simultaneously", result)
            return result
        }
        return true
    }
}

把navigationController.interactivePopGestureRecognizer?.delegate替换为NavigationInteractiveTransition类的对象,多操作几次,可以发现

  1. 在根视图控制器时,不接受touch
  2. 不接受UINavigationBar上的touch
  3. 当自定义左边按钮或隐藏UINavigationBar时,不接受touch
按照1和2修改代码:

    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        if navigationControllerShouldPop() == false {
            return false
        }
        if isTouchViewInNavigationBar(touch.view) {
            return false
        }
        return true
    }
    open func navigationControllerShouldPop() -> Bool {
        guard let navigationController = self.navigationController else {
            return false
        }
        if navigationController.viewControllers.count <= 1 {
            return false
        }
        return true
    }
    open func isTouchViewInNavigationBar(_ view: UIView?) -> Bool {
        guard let navigationController = self.navigationController else {
            return false
        }
        var temp = view
        while temp != nil {
            if navigationController.navigationBar.isEqual(temp) {
                return true
            }else {
                temp = temp?.superview
            }
        }
        return false
    }


到这里,已经解决了pop手势失效的问题了。

UIViewController决定是否可以pop

有时候会有这样的需求,有的页面不想让pop手势起作用。先定义一个协议InterruptNavigationControllerPopGesture。

public protocol InterruptNavigationControllerPopGesture {
    
    func interruptNavigationController(_ navigationController: UINavigationController, PopGesture: UIGestureRecognizer) -> Bool
    
}

修改NavigationInteractiveTransition的代码。

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if navigationControllerShouldInterruptPopGesture(gestureRecognizer) {
            return false
        }
        if let result = wrapped?.gestureRecognizerShouldBegin?(gestureRecognizer) {
            return result
        }
        return true
    }
    open func navigationControllerShouldInterruptPopGesture(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        guard let navigationController = navigationController else {
            return true
        }
        guard let object = navigationController.topViewController as? InterruptNavigationControllerPopGesture else {
            return false
        }
        return object.interruptNavigationController(navigationController, PopGesture: gestureRecognizer)
    }

让不想pop手势有效的类遵守协议InterruptNavigationControllerPopGesture,实现协议方法返回true打断UINavigationController的pop手势。

Demo见github