Swift 專案總結 05 系統邊緣右滑 pop 手勢調整

執著丶執念發表於2018-06-02

Swift 專案總結 05   系統邊緣右滑 pop 手勢調整

需求產生

某天,產品經理突然和我說,“我們的應用怎麼沒有右滑返回手勢?我看很多應用都有的”,

我想這個功能不是系統自帶的嗎?難道是出什麼問題了,我就在我手機上測試了一下,然後一臉你逗我呢的表情說:“有啊,你看,怎麼沒有?”,

他看我操作確實是可以,他就拿我手機試了一下,然後像發現新大陸一樣:“咦,我怎麼就不行?”,

我看著他操作,懟回去說,“你右滑返回是從螢幕中間偏左邊一點開始滑,肯定是不行的,要從螢幕邊緣開始滑。”,

然後他一臉原來這樣的表情,理所應當的說:“把它改成螢幕左邊一點就能右滑返回”。

謹記:這是個非常容易忽略的問題,系統的邊緣右滑 pop 手勢對某些使用者來說不太友好,因為身為開發,我是知道只能從邊緣右滑 pop 的,但很多使用者是不知道的,身為開發,應當更多的是以使用者的角度去考慮,而不是你用得可以就行。

需求解析

看到這個需求,我會先進行解析:

先看系統有沒有介面

先看系統右滑 pop 手勢有沒有暴露給我們一些屬性或者方法進行調整,如果有,那就再好不過了,可惜的是沒有,那先想到的就是自己自定義手勢替代系統手勢,因為該手勢是在 UINavigationController 裡,我就需要自定義 UINavigationController,在 viewDidLoad 裡完成我的自定義手勢新增。

考慮改動最小化

遮蔽系統手勢,自己自定義手勢最大的問題就是需要自己去完成系統手勢功能,但這個右滑 pop 功能自己實現起來是比較複雜的,懶惰的我是不想去實現的,那既要遮蔽系統右滑 pop 手勢,又要把它的功能也拿過來,這裡就要用到黑科技了,????

黑科技 1 - KVC 獲取私有屬性

KVC 大家應該都很熟悉,其核心就是 valueForKeyPath 可以獲取到類的所有屬性,不管是公有的還是私有的,哈哈,你的東西我都能拿到,藏著掖著是沒有用的

黑科技 2 - 根據 NSSelectorFromString 呼叫私有方法

因為系統手勢處理 pop 轉場的方法是不暴露出來給我們的,我們也沒辦法通過 @selector 來獲取到該方法標號,這時候 NSSelectorFromString 就派上用場了,NSSelectorFromString 是 objC 中常用來字串轉方法標號的方法,就算是私有方法,我也能通過該方法拿到它的方法標號,O(∩_∩)O哈哈~

解決需求

下面直接上程式碼

// 自定義導航控制器
class BasicNavigationController: UINavigationController {
    
    // 全域性控制是否需要右滑返回
    var popGestureRecognizerEnabled = true
    // 自定義右滑返回手勢
    var popRecognizer: UIPanGestureRecognizer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        replaceInteractivePopGestureRecognizer()
    }
    
    /// 替換系統右滑返回手勢為自定義右滑返回手勢
    fileprivate func replaceInteractivePopGestureRecognizer() {
        // 獲取系統邊緣右滑返回手勢
        guard let gesture = self.interactivePopGestureRecognizer, let gestureView = gesture.view else { return }
        // 讓系統右滑返回手勢失效
        gesture.isEnabled = false
        
        // 建立自己的右滑返回手勢,並新增到檢視上
        let popRecognizer = UIPanGestureRecognizer()
        popRecognizer.delegate = self
        popRecognizer.maximumNumberOfTouches = 1
        gestureView.addGestureRecognizer(popRecognizer)
        
        // 橋接系統右滑返回手勢的觸發方法到自己定義的手勢上
        var navigationInteractiveTransition: Any?
        // gesture._targets.first._target 就是系統右滑返回觸發方法所在的物件,因為涉及到隱式屬性,所以通過 valueForKey 的方式獲取
        if let targets = gesture.value(forKey: "_targets") as? NSMutableArray,
            let gestureRecognizerTarget = targets.firstObject as? NSObject {
            navigationInteractiveTransition = gestureRecognizerTarget.value(forKey: "_target")
        }
        if let navigationInteractiveTransition = navigationInteractiveTransition {
            // 因為 handleNavigationTransition 是 ObjC 的私有方法,這裡通過字串轉方法名的方式實現橋接
            let handleTransition = NSSelectorFromString("handleNavigationTransition:")
            popRecognizer.addTarget(navigationInteractiveTransition, action: handleTransition)
        }
        self.popRecognizer = popRecognizer
    }
}
複製程式碼

但這樣還沒有結束,我們還需要去修改手勢響應區域

extension BasicNavigationController: UIGestureRecognizerDelegate {
    
    /// 判斷是否觸發右滑返回手勢,條件:1. 方向是往右滑, 2. 控制器棧的高度要大於1, 3. 不在轉場過程中,
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        guard let popRecognizer = self.popRecognizer, popGestureRecognizerEnabled else { return false }
        
        // 1. 方向是往右滑
        guard popRecognizer.translation(in: self.view).x >= 0 else { return false }
        
        // 2. 控制器棧的高度要大於1
        guard self.viewControllers.count > 1 else { return false }
        
        // 3. 不在轉場過程中
        guard let isTransitioning = self.value(forKey: "_isTransitioning") as? NSNumber else { return false }
        return !isTransitioning.boolValue
    }
    
    /// 控制開始觸發右滑返回手勢的區域,這裡是左邊邊緣距離 1/3 螢幕寬度範圍內都能觸發
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        let point = touch.location(in: self.view)
        return point.x >= 0 && point.x < ceil(UIScreen.main.bounds.size.width) / 3.0
    }
}
複製程式碼

最後想到手勢響應區域擴大,必定會導致一些手勢衝突,最常見的手勢衝突就是和 UIScrollView 的右滑手勢衝突

extension BasicNavigationController: UIGestureRecognizerDelegate {   
    /// 解決 scrollView 的滑動手勢 和 右滑返回手勢 衝突問題
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        guard otherGestureRecognizer is UIPanGestureRecognizer else { return false }
        guard let otherGestureView = otherGestureRecognizer.view as? UIScrollView else { return false }
        guard otherGestureView.bounces && otherGestureView.alwaysBounceHorizontal else { return false }
        return otherGestureView.contentOffset.x <= 0
    }
}
複製程式碼

至此需求解決完畢,這裡是 Demo 原始碼:ScreenEdgePanGestureDemo

如果還有其他更好的辦法,可以在下方評論區留言,求關注求點贊,這裡是我的 github 地址 和微信 liutingluhe ,瞭解一下。

相關文章