需求產生
某天,產品經理突然和我說,“我們的應用怎麼沒有右滑返回手勢?我看很多應用都有的”,
我想這個功能不是系統自帶的嗎?難道是出什麼問題了,我就在我手機上測試了一下,然後一臉你逗我呢的表情說:“有啊,你看,怎麼沒有?”,
他看我操作確實是可以,他就拿我手機試了一下,然後像發現新大陸一樣:“咦,我怎麼就不行?”,
我看著他操作,懟回去說,“你右滑返回是從螢幕中間偏左邊一點開始滑,肯定是不行的,要從螢幕邊緣開始滑。”,
然後他一臉原來這樣的表情,理所應當的說:“把它改成螢幕左邊一點就能右滑返回”。
謹記:這是個非常容易忽略的問題,系統的邊緣右滑 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 ,瞭解一下。