最近遇到一個關於導航欄返回按鈕的問題,因為之前項目里面都是用的系統默認的返回按鈕樣式所以沒有想過要去更改,后來有需要將返回按鈕箭頭旁邊的文字去掉,同時將該返回按鈕的點擊事件重新定義。一開始嘗試自定義按鈕然后設置為leftBarButtonItem,但是這樣圖片可能跟系統自帶的不一樣,還有就是返回按鈕的位置跟系統自帶的不一樣。后來找了一些資料,發現將文字去掉比較簡單,一般做法是控制器中添加如下代碼,然后他的下一級控制就有一個只有箭頭沒有文字返回按鈕:
UIBarButtonItem *backBtn = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; self.navigationItem.backBarButtonItem = backBtn;
也可以創建一個根控制器在其中使用上述代碼然后讓其他控制器繼承這個根控制器實現批量操作。但是如果遇到需要自定義該返回的點擊事件的時候,在上面方法中添加target與action是不可行的,同時這種做法也會產生一個問題,就是實際的返回按鈕點擊區域總是比按鈕看到的范圍大,一般是距離箭頭右邊30距離都可點擊。接下來我就帶大家一起了解這些問題產生的原因以及如何更好的解決這些問題。大家也可以去我的github上下載相關的代碼:https://github.com/peanutNote/QYNavBackButton.git
首先我們看一下按照上面代碼去掉返回按鈕文字之后的導航欄視圖的結構層次,因為導航欄的視圖加載以及初始化跟viewController的view不一樣,不能再veiwDidLoad中去觀察(viewWillAppear中也不行)要在viewDidLoad中才可以看到完整的導航欄視圖結構層次。我們可以在一個有去掉文字的返回按鈕控制器的viewDidLoad中打上斷點然后在控制台執行:
po [[UIWindow keyWindow] recursiveDescription]
會得到如下輸出:
<UIWindow: 0x8d6f970; frame = (0 0; 320 480); autoresize = W+H; gestureRecognizers = <NSArray: 0x8d5dbf0>; layer = <UIWindowLayer: 0x8d717d0>> | <UILayoutContainerView: 0x8d7bbf0; frame = (0 0; 320 480); autoresize = W+H; gestureRecognizers = <NSArray: 0x8d78a70>; layer = <CALayer: 0x8d7bcd0>> | | <UINavigationTransitionView: 0x8d813f0; frame = (0 0; 320 480); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x8d814d0>> | | | <UIViewControllerWrapperView: 0x8d61050; frame = (0 0; 320 480); autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x8d88f40>> | | | | <UIView: 0x8ab0dc0; frame = (0 0; 320 480); autoresize = RM+BM; layer = <CALayer: 0x8ab0610>> | | | | | <_UILayoutGuide: 0x8ab0e20; frame = (0 0; 0 64); hidden = YES; layer = <CALayer: 0x8ab0e90>> | | | | | <_UILayoutGuide: 0x8ab1080; frame = (0 480; 0 0); hidden = YES; layer = <CALayer: 0x8ab10f0>> | | <UINavigationBar: 0x8d75c40; frame = (0 20; 320 44); opaque = NO; autoresize = W; userInteractionEnabled = NO; gestureRecognizers = <NSArray: 0x8d5e750>; layer = <CALayer: 0x8d70f00>> | | | <_UINavigationBarBackground: 0x8d59af0; frame = (0 -20; 320 64); opaque = NO; autoresize = W; userInteractionEnabled = NO; layer = <CALayer: 0x8d549f0>> - (null) | | | | <_UIBackdropView: 0x8d7c440; frame = (0 0; 320 64); opaque = NO; autoresize = W+H; userInteractionEnabled = NO; layer = <_UIBackdropViewLayer: 0x8d7e7b0>> | | | | | <_UIBackdropEffectView: 0x8d7f1c0; frame = (0 0; 320 64); clipsToBounds = YES; opaque = NO; autoresize = W+H; userInteractionEnabled = NO; animations = { filters.colorMatrix.inputColorMatrix=<CABasicAnimation: 0x8ba4490>; }; layer = <CABackdropLayer: 0x8d7f480>> | | | | | <UIView: 0x8d7fc80; frame = (0 0; 320 64); hidden = YES; opaque = NO; autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x8d7fce0>> | | | | <UIImageView: 0x8d67cc0; frame = (0 64; 320 0.5); userInteractionEnabled = NO; layer = <CALayer: 0x8d67d50>> - (null) | | | <UINavigationItemView: 0x8ab6400; frame = (124 8; 163 27); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x8ab6480>> | | | | <UILabel: 0x8ab64b0; frame = (0 3; 163 22); text = 'A Story About a Fish'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x8ab6550>> | | | <UINavigationItemButtonView: 0x8ab6c80; frame = (8 6; 41 30); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x8ab6d60>> | | | | <UILabel: 0x8ab6f10; frame = (-981 -995; 91 22); text = ''; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x8ab6fb0>> | | | <_UINavigationBarBackIndicatorView: 0x8d87560; frame = (8 12; 12.5 20.5); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x8d87650>> - Back
直接看或許不容易懂,還需要結合Xcode的“Debug view Hierarchy”或者是Reveal工具看實際視圖結構(如果你的導航欄右邊也添加了按鈕的話,這里還會多一個,在此不討論其余控件):
我們可以看到在UINavigationBar中包含有4個類,它們大致的作用是:
- _UINavigationBarBackground :(UIImageView)導航欄背景圖(不可以直接設置圖片)
- UINavigationItemView :(UIView)包含顯示導航欄標題
- UINavigationItemButtonView :(UIView)包含顯示導航欄左視圖(不可移除、更改大小、顏色,可以隱藏,決定了我們的自定義區域是否顯示)
- _UINavigationBarBackIndicatorView :(UIImageView)導航欄返回按鈕箭頭圖片(不可以修改圖片)
_UINavigationBarBackIndicatorView就是返回按鈕的箭頭也就是我們需要保留的,UINavigationItemButtonView就是就是決定導航欄要不要顯示返回按鈕的依據。再次看看這個對象在控制台的輸出:
| | | <UINavigationItemButtonView: 0x8ab6c80; frame = (8 6; 41 30); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x8ab6d60>> | | | | <UILabel: 0x8ab6f10; frame = (-981 -995; 91 22); text = ''; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x8ab6fb0>>
這個UINavigationItemButtonView應該是系統在這個view的draw方法里就決定frame,修改frame就觸發needdisplay重新改變它的frame,因此這個view只會根據其上的label(也就是返回按鈕顯示的文字)的內容變化而改變寬度其余基本不可變,我們雖然將label的內容設置為空但是這個UINavigationItemButtonView的大小卻並沒有改變同時點擊區域也沒有改變。從控制台里的還可看到這個veiw的userInteractionEnabled屬性為NO,很明顯我們的返回事件並不在這個view上,只能說明是真正響應點擊事件的是這個view背后添加了某個UIGestureRecognizer。因此要想解決我之前提到的問題首先我們得覆蓋這個默認的返回響應事件,然后添加我們自己的事件。同時還得想到的是當我們有需要自己定義左按鈕的時候就得移除這種覆蓋操作,因此我在這里創建了一個自定義的視圖QYMaskView覆蓋系統事件,然后通過UINavigationItemButtonView的存在與否來控制QYMaskView是否存在就可以實現以上效果
for (UIView *view in self.navigationController.navigationBar.subviews) { if ([view isKindOfClass:NSClassFromString(@"UINavigationItemButtonView")]) { nav_backView = view; } else if ([view isKindOfClass:[QYMaskView class]]) { nav_qyView = (QYMaskView *)view; } } if (nav_backView && !nav_qyView) { QYMaskView *qyButtonView = [[QYMaskView alloc] initWithFrame:CGRectMake(0, 0, 100, 44)]; qyButtonView.backButton = [UIButton buttonWithType:UIButtonTypeCustom]; qyButtonView.backButton.frame = CGRectMake(8, 6, 30, 30); [qyButtonView.backButton addTarget:self action:@selector(customNavBackButtonMethod) forControlEvents:UIControlEventTouchUpInside]; [qyButtonView addSubview:qyButtonView.backButton]; [self.navigationController.navigationBar addSubview:qyButtonView]; } else if (nav_backView && nav_qyView) { [nav_qyView.backButton removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents]; [nav_qyView.backButton addTarget:self action:@selector(customNavBackButtonMethod) forControlEvents:UIControlEventTouchUpInside]; } else if (!nav_backView && nav_qyView) { [nav_qyView removeFromSuperview]; }
通過在QYMaskView中添加按鈕來替換系統返回事件。其中有一步是給按鈕添加與移除按鈕事件,目的是為了加入我們自定義的事件。當視圖控制器顯示的時候我們會去給該按鈕重新添加點擊事件,由於customNavBackButtonMethod事件定義到了.h中,當我們在本類中也實現了這個方法,我們在分類中調用這個方法就會執行本類中的實現(因為此時該方法的定義已經被覆蓋)。因此只要在對應的viewDidLoad中導入
#import "UIViewController+QYCustomBackButton.h"
然后復寫customNavBackButtonMethod方法就可以在點擊返回按鈕時執行到你復寫的方法中了。最后總結出來的解決辦法就是創建一個viewController的分類:
UIViewController+QYCustomBackButton.h文件 #import <UIKit/UIKit.h> @interface QYMaskView : UIView @property (nonatomic, strong) UIButton *backButton; @end @interface UIViewController (QYCustomBackButton) - (void)customNavBackButtonMethod; @end UIViewController+QYCustomBackButton.m文件 #import "UIViewController+QYCustomBackButton.h" #import <objc/runtime.h> @implementation QYMaskView @end @implementation UIViewController (QYCustomBackButton) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(viewDidLoad); SEL swizzledSelector = @selector(qy_viewDidLoad); SEL originalSelector1 = @selector(viewDidAppear:); SEL swizzledSelector1 = @selector(qy_viewDidAppear); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); Method originalMethod1 = class_getInstanceMethod(class, originalSelector1); Method swizzledMethod1 = class_getInstanceMethod(class, swizzledSelector1); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); BOOL didAddMethod1 = class_addMethod(class, originalSelector1, method_getImplementation(swizzledMethod1), method_getTypeEncoding(swizzledMethod1)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } if (didAddMethod1) { class_replaceMethod(class, swizzledSelector1, method_getImplementation(originalMethod1), method_getTypeEncoding(originalMethod1)); } else { method_exchangeImplementations(originalMethod1, swizzledMethod1); } }); } #pragma mark - Method Swizzling - (void)qy_viewDidLoad { [self qy_viewDidLoad]; UIBarButtonItem *backButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; [self.navigationItem setBackBarButtonItem:backButtonItem]; } - (void)qy_viewDidAppear { [self qy_viewDidAppear]; UIView *nav_backView = nil; QYMaskView *nav_qyView = nil; for (UIView *view in self.navigationController.navigationBar.subviews) { if ([view isKindOfClass:NSClassFromString(@"UINavigationItemButtonView")]) { nav_backView = view; } else if ([view isKindOfClass:[QYMaskView class]]) { nav_qyView = (QYMaskView *)view; } } if (nav_backView && !nav_qyView) { QYMaskView *qyButtonView = [[QYMaskView alloc] initWithFrame:CGRectMake(0, 0, 100, 44)]; qyButtonView.backButton = [UIButton buttonWithType:UIButtonTypeCustom]; qyButtonView.backButton.frame = CGRectMake(8, 6, 30, 30); [qyButtonView.backButton addTarget:self action:@selector(customNavBackButtonMethod) forControlEvents:UIControlEventTouchUpInside]; [qyButtonView addSubview:qyButtonView.backButton]; [self.navigationController.navigationBar addSubview:qyButtonView]; } else if (nav_backView && nav_qyView) { [nav_qyView.backButton removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents]; [nav_qyView.backButton addTarget:self action:@selector(customNavBackButtonMethod) forControlEvents:UIControlEventTouchUpInside]; } else if (!nav_backView && nav_qyView) { [nav_qyView removeFromSuperview]; } } - (void)customNavBackButtonMethod { [self.navigationController popViewControllerAnimated:YES]; } @end
導入到項目中就可以生效了。這里所做的就是在所有的viewController執行viewDidLoad的時候將返回按鈕的文字置空,在執行viewDidAppear的時候添加一個自定義的按鈕去響應pop事件。好了,是不是感覺很簡單呢。當然在研究這一個問題過程中還是有很多我沒弄明白的地方,可能在各位同學使用的時候產生各種問題,給大家帶來的不便敬請諒解,同時歡迎大家提出寶貴的建議予以改進,在此感激不盡!