iOS 實現UINavigation全屏滑動返回(二)

LinXunFeng發表於2019-02-22

回顧

iOS – 實現UINavigation全屏滑動返回(一) 中我們實現了滑動返回的功能,但不是全屏滑動返回,得在左側邊緣輕掃才能滑動返回~UINavigationController自帶的只能在邊緣輕掃才能滑動返回,這使用者體驗是不好的,接下來實現全屏滑動返回!

思路

既然自帶的滑動返回只能是在邊緣,那我們能不能修改使它觸控範圍變大甚至全屏呢?先來看下系統手勢有沒有提供屬性或方法供我們使用

NSLog(@"%@", self.interactivePopGestureRecognizer);
複製程式碼

列印資訊:

/*
<UIScreenEdgePanGestureRecognizer: 0x7fd542611e20; state = Possible;
 delaysTouchesBegan = YES; view = <UILayoutContainerView 0x7fd542706300>; target= 
<(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 
0x7fd542611ce0>)>>
*/
複製程式碼

原來系統手勢的型別為 UIScreenEdgePanGestureRecognizer ,轉到定義,發現有一個屬性

UIRectEdge edges
複製程式碼

是個結構體

typedef NS_OPTIONS(NSUInteger, UIRectEdge) { 
   UIRectEdgeNone = 0,
   UIRectEdgeTop = 1 << 0,
   UIRectEdgeLeft = 1 << 1,
   UIRectEdgeBottom = 1 << 2,
   UIRectEdgeRight = 1 << 3,
   UIRectEdgeAll = UIRectEdgeTop | UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight
}
複製程式碼

只提供了這幾樣,都是邊緣的,這樣就只好另尋他路了。
既然沒有提供方式給我們現實要求,那我們就自己新增一個拖動手勢 UIPanGestureRecognizer來替它執行滑動返回功能。

UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:target action:@selector(handleNavigationTransition:)];
[self.view addGestureRecognizer:pan];
複製程式碼

新增一個拖動手勢,讓他執行系統手勢的操作,呼叫handleNavigationTransition:方法(剛才列印的資訊中可以得知),現在的問題就是target是誰?
我們可以看看UIScreenEdgePanGestureRecognizer中是否有線索呢?

UIScreenEdgePanGestureRecognizer *gest = self.interactivePopGestureRecognizer;
複製程式碼

找了半天沒找著,但是UIScreenEdgePanGestureRecognizer繼承於UIPanGestureRecognizer,而UIPanGestureRecognizer又繼承於UIGestureRecognizer,在UIGestureRecognizer提供的方法中我們可以推斷出一定有target,而且還是強引用的私有屬性!那我們就可以用OC強大的殺手鐗KVC來得到這個屬性,但是前提是我們得知道target所指屬性是什麼名字
參照我的另一篇文章:iOS – 通過runtime獲取某個類中所有的變數和方法

// OC runtime 機制
// 只能動態獲取當前類的成員屬性,不能獲取其子類,或者父類的屬性
unsigned int count = 0;// 拷貝出所胡的成員變數列表
Ivar *ivars = class_copyIvarList([UIGestureRecognizer class], &count);
for (int i = 0; i<count; i++) {
   // 取出成員變數
   Ivar ivar = *(ivars + i); 

   // 列印成員變數名字
   NSLog(@"%s", ivar_getName(ivar)); 

   // 列印成員變數的資料型別
   NSLog(@"%s", ivar_getTypeEncoding(ivar));
}

 // 釋放
 free(ivars);
複製程式碼

在列印中我們找到了UIGestureRecognizer的私有屬性 _targets,是個陣列,而且只有一個元素,元素的型別如圖所示

target

那就好辦了,這樣我們就可以得到target了

NSArray *targets = [gest valueForKeyPath:@"_targets"]; // 列印可以發現裡面就一個元素
id target = [targets[0] valueForKeyPath:@"_target"];
複製程式碼

這樣我們就差不多實現全屏滑動返回的功能,但是有個bug

向右滑動,接著點選Button

如圖所示,在最後裡回到根控制器介面後我再一次向右滑動,接著點選Button,它沒有將FirstVC彈出,這就是傳說中的bug,那我們現在在做的,就是在根控制器不讓滑動返回生效,即禁用手勢。
監聽手勢,遵守協議UIGestureRecognizerDelegate,實現代理方法

// 噹噹前控制器是是根控制器時不讓移除當前控制器(換句話說就是禁止手勢)
pan.delegate = self;
複製程式碼
#pragma mark - UIGestureRecognizerDelegate// 當開始滑動時呼叫
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
   // 當為根控制器是不讓移除當前控制器,非根控制器時允許移除
   NSLog(@"%ld", self.viewControllers.count);
   BOOL open = self.viewControllers.count > 1;
   return open;
}
複製程式碼

最後說兩句

這樣就可以全屏滑動了,不過讓我們來看看我們新增手勢的習慣

UIPanGestureRecognizer *myPan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan)];
[self.view addGestureRecognizer:myPan];
myPan.delegate = self;
複製程式碼

我們在新增手勢時設定了target為self,而delegate也為self
那是不是可以推斷出系統手勢的delegate就是我們剛剛想要的target呢,答案是是的

id target = self.interactivePopGestureRecognizer.delegate;
複製程式碼

所以我們的target就可以通過這種方式獲得,不用KVC的方式
哦,最後別忘了禁用系統手勢

// 禁止系統的手勢
self.interactivePopGestureRecognizer.enabled = NO;
複製程式碼

這樣,我們就實現了全屏滑動返回的功能了~

原始碼

Objective-C

記得遵守協議: UIGestureRecognizerDelegate
LXFNavigationController.m

- (void)viewDidLoad {
   [super viewDidLoad];
   // 系統的手勢
   UIScreenEdgePanGestureRecognizer *gest = self.interactivePopGestureRecognizer;
   // target
   id target = self.interactivePopGestureRecognizer.delegate;
   // 禁止系統的手勢 
   self.interactivePopGestureRecognizer.enabled = NO;
   UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:target action:@selector(handleNavigationTransition:)];
   [self.view addGestureRecognizer:pan]; 
   // 監聽代理
   pan.delegate = self;
}
複製程式碼
#pragma mark - UIGestureRecognizerDelegate
// 當開始滑動時呼叫
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
   // 當為根控制器是不讓移除當前控制器,非根控制器時允許移除
   NSLog(@"%ld", self.viewControllers.count);
   BOOL open = self.viewControllers.count > 1;
   return open;
}
複製程式碼

Swift

LXFNavigationController.swift

override func viewDidLoad() {
    super.viewDidLoad()
    
    guard let targets = interactivePopGestureRecognizer!.value(forKey: "_targets") as? [NSObject] else { return }
    let targetObjc = targets.first
    let target = targetObjc?.value(forKey: "target")
    let action = Selector(("handleNavigationTransition:"))
    
    let panGes = UIPanGestureRecognizer(target: target, action: action)
    view.addGestureRecognizer(panGes)
}
複製程式碼

相關文章