一、場景介紹
現(xiàn)在大多數(shù)APP 都有一個需求,就是隱藏某一個頁面的NavigationBar。很多開發(fā)者直接 [self.navigationController setNavigationBarHidden:YES] 就萬事大吉了。但是如果開發(fā)者試著將邊緣側滑返回功能加上之后,細心的同學就會發(fā)現(xiàn),如果我們在一個隱藏NavigationBar的頁面和一個顯示NavigationBar 的頁面通過手勢來回切換后,再繼續(xù)push到更深層的頁面,頂部的NavigationBar就會出現(xiàn)錯亂的情況。因此好多APP 將側滑返回直接去掉了,但是這樣的話,整個APP 就會顯得比較粗糙,體驗不佳。其實早已iOS自帶了側滑返回功能,只不過大多數(shù)時候由于我們自定義了NavigationBar的leftBarButtonItem,導致了轉場交互代理interactivePopGestureRecognizer失效了,需要我們重新指定一下代理,在此不再贅述。
二、問題解決
為了更好的描述問題,我特地定義了兩個控制器:1、ViewController1(隱藏了導航欄,導航控制器的根控制器) ;2、ViewController2(需要顯示導航控制器,由ViewController1 push 而來)。 那么怎么解決“側滑返回功能”與“隱藏NavigationBar”共存呢?我的思路是:既然再同一個導航控制器棧內會出問題,那么我將ViewController1和ViewController2放在兩個導航控制器里不就可以了嗎?但是我們還想需要push的效果怎么辦?導航控制器是不允許在push一個新的導航控制器的。只能present出來一個新的導航控制器。因此我想到了自定義一個轉場動畫,讓present出來的效果如同push的效果一樣。
1)設置側滑返回
#import <UIKit/UIKit.h>
@protocol ModalViewControllerDelegate <NSObject>
-(void) modalViewControllerDidClickedDismissButton:(UIViewController *)viewController;
@end
@interface BaseNavigationController : UINavigationController<UINavigationControllerDelegate,UIGestureRecognizerDelegate>
//@property(nonatomic,weak)UIPanGestureRecognizer *popPan;
//轉場代理
@property (nonatomic, weak) id<ModalViewControllerDelegate> transDelegate;
@end
#import "BaseNavigationController.h"
@interface BaseNavigationController ()
@property(nonatomic,weak) UIViewController* currentShowVC;
@end
@implementation BaseNavigationController
- (void)viewDidLoad {
[super viewDidLoad];
//請忽略這段設置UI樣式的代碼,沒什么卵用
[self.navigationBar setBarTintColor:[Theme colorOne]];
[self.navigationBar setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
[UIColor whiteColor],NSForegroundColorAttributeName, nil]];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent animated:YES];
[self.navigationBar setTranslucent:NO];
<br>
self.view.layer.shadowColor = [UIColor blackColor].CGColor;
self.view.layer.shadowOffset = CGSizeMake(-2, 0);
self.view.layer.shadowOpacity = 0.3;
}
//導航控制器一被創(chuàng)建則設置交互手勢代理
-(id)initWithRootViewController:(UIViewController *)rootViewController
{
BaseNavigationController* nvc = [super initWithRootViewController:rootViewController];
self.interactivePopGestureRecognizer.delegate = self;
nvc.delegate = self;
return nvc;
}
//這段主要是判斷為了幫助手勢判斷當前響應手勢的控制器是不是根視圖控制器
-(void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if (navigationController.viewControllers.count == 1)
self.currentShowVC = Nil;
else if(animated){
self.currentShowVC = viewController;
}else{
self.currentShowVC = Nil;
}
}
//交互手勢代理(告訴手勢什么時候需要響應,什么時候不響應)
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer == self.interactivePopGestureRecognizer) {
return (self.currentShowVC == self.topViewController);
}
return YES;
}
設置側滑返回我們需要在base類里重新設置代理,并實現(xiàn)轉場手勢的代理,告訴手勢當頁面為根視圖控制器時候手勢不響應,其實以上代碼就是實現(xiàn)這么點的功能,僅此而已。
2)設置轉場動畫
轉場動畫我是直接修改了大神的代碼 。以下是我修改后的代碼,只是修改了動畫部分,使用方法是一致的。
#import "BouncePresentAnimation.h"
@implementation BouncePresentAnimation
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
return 0.25f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
// 1. Get controllers from transition context
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// 2. Set init frame for toVC
CGRect screenBounds = [[UIScreen mainScreen] bounds];
CGRect finalFrame = [transitionContext finalFrameForViewController:toVC];
toVC.view.frame = CGRectOffset(finalFrame, screenBounds.size.width, 0);
// 3. Add toVC's view to containerView
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];
// 4. Do animate now
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
toVC.view.frame = finalFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
#import "NormalDismissAnimation.h"
@implementation NormalDismissAnimation
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
return 0.25f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
// 1. Get controllers from transition context
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// 2. Set init frame for fromVC
CGRect screenBounds = [[UIScreen mainScreen] bounds];
CGRect initFrame = [transitionContext initialFrameForViewController:fromVC];
CGRect finalFrame = CGRectOffset(initFrame, screenBounds.size.width,0 );
// 3. Add target view to the container, and move it to back.
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];
[containerView sendSubviewToBack:toVC.view];
// 4. Do animate now
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
fromVC.view.frame = finalFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
3)又一個坑
大家將轉場動畫和側滑功能加入后會發(fā)現(xiàn),當我在ViewController2中再繼續(xù)push時,如果側滑則直接返回到了我們的ViewController1控制器,而不是我們想要的次棧頂控制器。這涉及到了手勢之間競爭的問題,也就是說轉場的手勢取代我們邊緣側滑手勢去響應了,因此我們要告訴系統(tǒng),如果當前手勢所在的控制器不是present出來的導航控制器的根控制器的話,轉場手勢就不需要響應。因此需要修改大神的一部分代碼
#import "SwipeUpInteractiveTransition.h"
@interface SwipeUpInteractiveTransition()<UIGestureRecognizerDelegate>
@property (nonatomic, assign) BOOL shouldComplete;
@property (nonatomic, strong) UIViewController *presentingVC;
@end
@implementation SwipeUpInteractiveTransition
-(void)wireToViewController:(UIViewController *)viewController
{
self.presentingVC = viewController;
[self prepareGestureRecognizerInView:viewController.view];
}
- (void)prepareGestureRecognizerInView:(UIView*)view {
UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
gesture.delegate = self;
[view addGestureRecognizer:gesture];
}
-(CGFloat)completionSpeed
{
return 1 - self.percentComplete;
}
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer {
CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:
// 1. Mark the interacting flag. Used when supplying it in delegate.
self.interacting = YES;
[self.presentingVC dismissViewControllerAnimated:YES completion:nil];
break;
case UIGestureRecognizerStateChanged: {
// 2. Calculate the percentage of guesture
CGSize screenSize = [UIScreen mainScreen].bounds.size;
CGFloat fraction = translation.x / screenSize.width;
//Limit it between 0 and 1
fraction = fminf(fmaxf(fraction, 0.0), 1.0);
self.shouldComplete = (fraction > 0.5);
[self updateInteractiveTransition:fraction];
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
// 3. Gesture over. Check if the transition should happen or not
self.interacting = NO;
if (!self.shouldComplete || gestureRecognizer.state == UIGestureRecognizerStateCancelled) {
[self cancelInteractiveTransition];
} else {
[self finishInteractiveTransition];
}
break;
}
default:
break;
}
}
//如果不是根視圖控制器則不響應
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
if ([self.presentingVC isKindOfClass:[BaseNavigationController class]]) {
BaseNavigationController *nav = (BaseNavigationController*)self.presentingVC;
if (nav.viewControllers.count >=2) {
return NO;
}
}
return YES;
}
至此側滑和導航欄的隱藏則完美兼容,希望這篇文章對你有用~