【iOS】UINavigationController全屏侧滑&侧滑监听

在日常开发中,我们经常使用到系统的导航,来完成各页面的交互跳转,这是再正常不过且用到的基础功能。众所周知,我们也可以在具体的页面禁用系统导航,一般都是通过设置interactivePopGestureRecognizer是否开启来实现,不过,我们必须注意系统导航在根页面时,未经处理,用户再测滑则会导致app卡住甚至卡死,这个问题在之前的一篇文章中也有说过,并且也说了如何监听用户侧滑释放的方法,以便我们能够做一些额外操作。文章链接🔗: iOS interactivePopGestureRecognizer卡住&手势滑动监听

尽管系统的导航已满足大部分开发需求,但是用户的体验可能不是很好。之前的那篇文章虽然解决了以下两个问题:

  • 导航在根页面时因为测滑卡死。
  • 导航测滑监听的解决方案。

但是发现还是不够完美,因此,本文主要结合实际,实现系统的侧滑导航、全屏侧滑导航、导航侧滑监听的一体化方案,解决以下问题:

  • 避免在根页面测滑卡死。
  • 导航侧滑的更好的监听解决方案。
  • 实现全屏测滑导航。

系统测滑导航

首先,在子类BaseNavigationController中设置手势代理和导航代理


@interface BaseNavigationController ()<UIGestureRecognizerDelegate, UINavigationControllerDelegate>

@end

- (void)viewDidLoad {
    [super viewDidLoad]; 
    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
        self.interactivePopGestureRecognizer.delegate = self;
        self.delegate = self;
    }
}

为了保证每一次push操作,都设置开启系统导航,我们复写父类UINavigationController的方法

// Override super method to initialize interactive pop gesture here.
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
    [super pushViewController:viewController animated:animated];
    self.interactivePopGestureRecognizer.enabled = YES;
}

这样的话,就保证了每次即将push到下一个页面时,系统测滑手势是被开启的。

实现UINavigationControllerDelegate的方法

#pragma mark UINavigationControllerDelegate

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
        // If viewController is rootViewController, disable the pop gesture recognizer.
        self.interactivePopGestureRecognizer.enabled = self.viewControllers.count > 1 && self.interactivePopGestureRecognizer.isEnabled;
    }
}

这样就可以了吗?经测试,我们发现,虽然以上方案解决了根页面测滑卡死的问题,但是有一个BugA页面 -> B页面 -> C页面,如果C页面禁用测滑手势,即设置self.navigationController.interactivePopGestureRecognizer.enabled = NO;,此时返回到B页面时,发现B页面的测滑手势也被禁用了,即无法侧滑到A页面。这是因为侧滑手势的属性设置时全局性的,都是导航控制器的同一个设置,所以才出现了这么一个Bug

既然C页面设置了禁用侧滑手势,导致C页面销毁时,所有的测滑手势都被禁用,那能不能在C页面消失的时候再把测滑手势开启呢?答案显然是可以的,比如

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}

但是,如果有多个页面都需要禁用测滑手势,其他都是开启,我们要每个页面都这样写一遍?显然太麻烦,而且都是些冗余代码。因此,我的思路是,在父类中用一个变量保存每一个控制器的侧滑手势,如下

@implementation BaseNavigationController {
    NSMutableDictionary <NSString *, NSNumber *> *vcsDic;
}

vcsDic = @{}.mutableCopy;

#pragma mark UINavigationControllerDelegate

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    NSString *vcKey = NSStringFromClass(viewController.class);
    if (vcKey && vcsDic[vcKey] == nil) {
        // Saves each pop gesture enabled value if it's set for child view controller.
        vcsDic[vcKey] = @(self.interactivePopGestureRecognizer.isEnabled);
    }
    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
        // If viewController is rootViewController, disable the pop gesture recognizer.
        self.interactivePopGestureRecognizer.enabled = self.viewControllers.count > 1 && vcsDic[vcKey].boolValue;
    }
}

这样就完美地解决了测滑手势设置的相互之间的影响问题。接下来,我们看一下如何实现全屏手势侧滑。

全屏测滑导航

我们知道,系统测滑手势必然要依赖手势识别器,而在导航控制器的源码中

@property(nullable, nonatomic, readonly) UIGestureRecognizer *interactivePopGestureRecognizer

可知,interactivePopGestureRecognizer就是手势的核心,我们在viewDidLoad打印一下

- (void)viewDidLoad {
    [super viewDidLoad]; 
    ....................
    NSLog(@"result:%@", self.interactivePopGestureRecognizer);
}

result:
<_UIParallaxTransitionPanGestureRecognizer: 0x1275096e0;
 state = Possible; 
 delaysTouchesBegan = YES; 
 view = <UILayoutContainerView 0x12990a3e0>;
 target= <(action=handleNavigationTransition:, 
 target=<_UINavigationInteractiveTransition 0x127507ac0>)>>

发现这个手势中有一个handleNavigationTransition:方法,从字面意思就是操作导航过渡。既然如此,我们就直接对这个方法进行操作,添加到我们当前导航的手势上面

- (void)configureNavigationGestures {
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    SEL transitionSel = @selector(handleNavigationTransition:);
#pragma clang diagnostic pop
    
    id target = self.interactivePopGestureRecognizer.delegate;
    if ([target respondsToSelector:transitionSel]) {
        UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:target action:transitionSel];
        pan.delegate = self;
        [self.view addGestureRecognizer:pan];
        self.delegate = self;
    }
}

然后实现手势代理UIGestureRecognizerDelegate

#pragma mark - UIGestureRecognizerDelegate

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    // If root view controller, disable pop gesture.
    if (self.viewControllers.count == 1) {
        return NO;
    }
    NSString *vcKey = NSStringFromClass(self.topViewController.class);
    if (vcKey && vcsDic[vcKey] != nil) {
        return vcsDic[vcKey].boolValue;
    }
    return self.interactivePopGestureRecognizer.isEnabled;
}

这样就实现了全屏侧滑导航了。

侧滑手势监听

上一篇文章中,iOS interactivePopGestureRecognizer卡住&手势滑动监听,尽管可以通过通知监听用户侧滑导航,但是实在是太不方便了,因为不仅要在导航的父类添加要监听的页面的字符串,还要在相应的页面做监听,还要移除监听,简直太繁琐了!

于是,针对这个问题,有了另一个思路,通过获取到正在测滑导航的页面,判断是否实现了相应的侧滑导航结束的方法,如果有,直接调用,如果没有,则什么也不做,太完美了!

但是有一个问题,我们如何知道当前正在侧滑的页面是哪一个呢?通过navigationController.topViewController并获取不到正在侧滑的页面,于是经过查看源码发现,系统提供的UIViewControllerTransitionCoordinatorContext有一个方法- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;可以获取到当前正在侧滑导航的UIViewController,这样就好实现了。

一般情况下,我们的所有页面都会有一个基类BaseViewController,我们在BaseViewController.h中添加

- (void)cl_popGestureDidEnd;

BaseViewController.m

/**
 * @brief NOTE: please override this method in your child view controller. Don't remove this method because it may be called if necessary.
*/
- (void)cl_popGestureDidEnd {
    // Attention! Override this method in child view controller.
}

在导航基类BaseNavigationController中导入

#import "BaseNavigationController.h"
#import "BaseViewController.h"

在监听导航侧滑的UINavigationControllerDelegate的方法中,实现

#pragma mark - UINavigationControllerDelegate

- (void)navigationController:(UINavigationController *)navigationController
      willShowViewController:(UIViewController *)viewController
                    animated:(BOOL)animated {
    [viewController.transitionCoordinator notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        if (context.isCancelled) return;
        UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
        if ([fromVC isKindOfClass:[BaseViewController class]]) {
            BaseViewController *baseVC = (BaseViewController *)fromVC;
            if ([baseVC respondsToSelector:@selector(cl_popGestureDidEnd)]) {
                [baseVC cl_popGestureDidEnd];
            }
        }
    }];
}

我们在想要监听侧滑导航的页面,比如SecondViewController(继承自BaseViewController),复写

- (void)cl_popGestureDidEnd {
    // 侧滑导航结束,即将pop到上一个页面,在这里做一些事吧...
}

这样,就实现了侧滑导航的监听,如果侧滑结束的时候,页面消失,即返回到上一个页面,cl_popGestureDidEnd方法就会被调用。

如何不想导入BaseViewController怎么办呢?也可以通过runtime运行时来做处理

- (void)navigationController:(UINavigationController *)navigationController
      willShowViewController:(UIViewController *)viewController
                    animated:(BOOL)animated {
    [viewController.transitionCoordinator notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        if (context.isCancelled) return;
        UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
        SEL sel = @selector(cl_popGestureDidEnd);
        if (fromVC && [fromVC respondsToSelector:sel]) {
            IMP imp = [fromVC methodForSelector:sel];
            void (*func)(id, SEL) = (void *)imp;
            func(fromVC, sel);
        }
    }];
}

BaseViewController.hBaseViewController.m中声明和实现cl_popGestureDidEnd方法,主要是为了子类可以直接调用,能够直接联想出来。如果使用runtime运行时的方案来实现的话,你也可以完全不用在BaseViewController中添加cl_popGestureDidEnd方法,直接在子类中实现cl_popGestureDidEnd方法即可。当然,不导入BaseViewController也可以直接使用runtime来调用,这里就不赘述了。

小结

本文主要介绍了全屏侧滑的实现,以及如何避免侧滑导航到根视图控制器app卡死的解决方案,最后也完美地实现如何监听用户侧滑导航的方法。全屏侧滑导航在一定程度上优化了用户体验,侧滑导航监听可以在一些特殊情况下,实现我们的一些特殊操作。

最后,本文将以上内容做了分开讲解,实际上我已经在Demo中将上面的代码完全整合到了一起,代码非常简单,总的一百多行代码。比如在Demo中有如下属性

/// Whether to use system pop gesture. If NO, full-screen pop gesture will be set.
static const BOOL useSystemGesture = NO;
/// Whether to enable global pop gestures. The defaut is YES.
static const BOOL popGestureEnabled = YES;

使用的时候,直接修改这两个属性即可,其余代码基本上直接使用,不需要修改!例如,想使用系统侧滑导航,直接修改

static const BOOL useSystemGesture = YES;

想使用全屏侧滑导航

static const BOOL useSystemGesture = NO;

默认开启全局侧滑手势

static const BOOL popGestureEnabled = YES;

默认关闭全局侧滑手势

static const BOOL popGestureEnabled = NO;

附录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342