声明:我为这个框架写了四篇文章:
第一篇:[iOS]UINavigationController全屏pop之为每个控制器自定义UINavigationBar
第二篇:[iOS]UINavigationController全屏pop之为每个控制器添加底部联动视图
第三篇:[iOS]UINavigationController全屏pop之为控制器添加左滑push
第四篇:[iOS]调和 pop 手势导致 AVPlayer 播放卡顿
[图片上传失败...(image-1f079b-1514007266422)]
框架特性
✅ 全屏 pop 手势支持
✅ 全屏 push 到绑定的控制器支持
✅ 为每个控制器定制 UINavigationBar 支持(包括设置颜色和透明度等)
✅ 为每个控制器添加底部联动视图支持
✅ 自定义 pop 手势范围支持(从屏幕最左侧开始计算宽度)
✅ 为单个控制器关闭 pop 手势支持
✅ 为所有控制器关闭 pop 手势支持
❤️ 当当前控制器使用 AVPlayer 播放视频的时候, 使用自定义的 pop 动画以保证 AVPlayer 流畅播放.
[图片上传失败...(image-53b3e8-1514007266422)]
Tips : 阅读本文之前,建议先阅读我之前的文章。因为这篇文章是基于之前文章继续封装的。_
Q&A:Demo里都有那些东西?
01、关于自定义导航栏
- 01、第一个控制器的导航条是透明的,第二个控制器的导航条是白色的,第三个控制器的导航条是橙色的。
所以,为每个控制器定制自己的导航条。 - 02、支持全屏右滑,这简直是必须的。关于全屏右滑,最详细,也最早探究这个问题的,我了解到的是 J_雨 ,他应该是全屏右滑的鼻祖。
- 03、最重要的一点,要求全屏右滑返回的时候,导航条跟随自己的控制器流畅的滑动。
- 04、以下属于更新的内容,为全屏右滑Pop手势添加滑动起始点控制API。也即是,你可以精确控制从离屏幕左侧起多长距离内右滑,Pop手势是有效的。
- 05、某些同学和我说,他们在某些界面需要临时关闭Pop手势,让我处理一下。现在已经处理好了,你可以临时关掉Pop手势,等你需要的时候再次打开。
02、此次新增加自定义底部联动视图
[图片上传失败...(image-53362e-1514007266422)]
现有问题
- 某些同学做购物软件,可能需要在某个控制器屏幕最下方一直悬浮一个“立即购买”、“马上咨询”之类的控件。有经验的同学遇到此类功能时可能会采用新建一个Window,然后再把自己需要的控件添加到这个Window上,然后设置这个Window的hidden = NO,就可以实现了。确实是这样,其实除了新建一个Window,你还可以将自己的控件添加到当前KeyWindow上,同样可以实现这个功能。
- 这个功能实现了,但是有一点很恶心的是,苹果自从iOS7开始,有了右滑手势这个东西。这个时候,相比于安卓前进后退按钮是单一的在窗口显示一个控制器,右滑手势可以让窗口同时显示两个控制器的View。
- 如果你按照上面添加Window或是添加在KeyWindow上的方式。第一、你就不能让你定义的这个控件随着用户右滑而右滑。第二,你只能在这两个方法里控制你的控件的显示和隐藏。但是试过以后你会发现,当用户右滑的时候,你的控件一直在跳跃闪烁。但是我们是有追求的程序狗,我们显然会对这种方案SAY:FUCK AWAY!THIS IS SO UGLY MAN. AND IT SMELLS LIKE SHIT.
-(void)viewWillAppear:(BOOL)animated
-(void)viewWillDisappear:(BOOL)animated
解决方案
- 此次我结合之前的自定义导航栏,给出了一个比较“优雅”的解决方案:
- 假如A是根控制器,B是下一个要Push过去的控制器,你只需要在A里Push方法后写如下一行。你就可以拥有一个和导航栏一样跟随用户右滑而流畅滑动的“联动”TabBar了,并且你不用关心这个TabBar 的显示和隐藏。
[self.navigationController pushViewController:YourOwnVC animated:YES];
YourOwnVC.navigationController.jp_linkViewHeight = YourHopeHeight;
01、之前文章讲了啥?
看到我这篇文章的同学99%都是没有看过我之前那篇文章的,所以我有必要再重新描述下之前文章的内容。但是如果你要详细了解实现思路还是需要回头看我那篇文章,因为这篇文章我是讲底部联动视图的,所以NavgationController不会讲的很细。
之前的文章大致说清楚了,怎么样去适应需要为每个界面都定制导航条的需求。
实现思路如下:
- 当我们调用这个initWithRootViewController构造方法的时候,先用一个JPNavigationController作为根控制器,负责所有的Push和Pop操作。
[[JPNavigationController alloc]initWithRootViewController:vc]; - 然后当用户调用Push方法的时候,先将用户传进来的控制器A用一个JPWarpNavigationController(继承UINavigationController)包装起来成为B,然后再将B用一个JPWarpViewController包装起来成为C,然后用根导航控制器JPNavigationController来Push和Pop。
- 经过这样包装以后,每一个控制器都拥有一个自己的导航条,所以,用户可以对每个导航条进行定制,比如说透明、颜色、渐变等操作。
- 之所以要进行这两层包装的原因是苹果默认的规则是导航控制器不能Push和Pop导航控制器。
02、导航栏更新部分实现思路?
实现目标:1.临时关闭Pop手势 2.自定义右滑手势有效区域
- 系统的interactivePopGestureRecognizer只提供了一个enabled的属性给我们,所以如果想要做更多的事就要想其他的办法。在这种情况下,我选择了自定义一个UIPanGestureRecognizer,替换系统的手势的实现
// 自定义的pop手势
-(UIPanGestureRecognizer *)jp_fullscreenPopGestureRecognizer{
if (!_jp_fullscreenPopGestureRecognizer) {
_jp_fullscreenPopGestureRecognizer = [UIPanGestureRecognizer new];
_jp_fullscreenPopGestureRecognizer.maximumNumberOfTouches = 1;
}
return _jp_fullscreenPopGestureRecognizer;
}
-(void)viewDidLoad{
[super viewDidLoad];
// 彻底隐藏导航栏
[self setNavigationBarHidden:YES];
// 添加pop手势(懒加载)
if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.jp_fullscreenPopGestureRecognizer]) {
[self.interactivePopGestureRecognizer.view addGestureRecognizer:self.jp_fullscreenPopGestureRecognizer];
// 用自己的手势替换系统的pop
NSArray *targets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
id target = [targets.firstObject valueForKey:@"target"];
SEL action = NSSelectorFromString(@"handleNavigationTransition:");
self.jp_fullscreenPopGestureRecognizer.delegate = [self jp_popGestureRecognizerDelegate];
[self.jp_fullscreenPopGestureRecognizer addTarget:target action:action];
// 系统手势置为不可用
self.interactivePopGestureRecognizer.enabled = NO;
}
}
- 这样我们用自己的手势替代了系统的右滑功能,所以我们可以在自定义的手势代理方法中自定义右滑的特性:
-(BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer{
// 根控制器不允许pop
if (self.navigationController.viewControllers.count <= 1) {
return NO;
}
// 当用户禁止的时候,不允许pop
if (!self.navigationController.jp_fullScreenPopGestureEnabled) {
return NO;
}
// 当开始触发的点大于用户指定的最大触发点的时候,禁止pop
CGPoint beginningLocation = [gestureRecognizer locationInView:gestureRecognizer.view];
CGFloat maxAllowedInitialDistance = self.navigationController.jp_interactivePopMaxAllowedInitialDistanceToLeftEdge;
if (maxAllowedInitialDistance >= 0 && beginningLocation.x > maxAllowedInitialDistance) {
return NO;
}
// 正在做过渡动画的时候禁止pop
if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
return NO;
}
// 反向滑动禁止pop
CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
if (translation.x <= 0) {
return NO;
}
// 暂时关闭pop手势禁止pop
if (self.closePopForTemporary) {
return NO;
}
return YES;
}
03、自定义底部联动视图?
- 自定义的底部联动视图应该必须满足两点:1.跟随用户滑动而流畅的滑动 2.不需要在viewWillAppear和viewWillDisappear中处理TabBar的显示和隐藏。
- 要满足以上两点,首先要考虑的是,我们的底部联动视图的父控件应该是谁?
- 从以上的分析大概也可以知道如果我们把TabBar添加到导航栏上,我们需要达到的要求就都满足了。
- 为了方便大家使用,我先定义一个占位容器视图JPLinkContainerView,我把它添加到导航栏上,作为导航栏的子控件,以后使用的时候,~~只需要往这个占位视图上添加你自己的TabBar控件就可以了。~~ 你只需要把你的联动视图传给框架就可以了。
- 当我们把联动的JPLinkContainerView添加到导航条上以后,联动视图就超出父控件的范围,不能响应点击事件了。所以首先我们要做的就是自定义一个JPNavigationBar替换掉系统的导航条。借助于KVC,我们可以实现这一点。
JPNavigationBar *navBar = [[JPNavigationBar alloc]init];
[self setValue:navBar forKey:@"navigationBar"];
* 现在我们自定义了导航条,我们就可以在JPNavigationBar中重载hitTest方法,在这个方法中遍历它自身的子控件,找到我们添加的JPLinkContainerView。当点击的点在我们添加的占位容器视图JPLinkContainerView身上的时候,我们就手动把点击事件分发给我们自定义的TabBar子控件上,完成响应者链条的事件传递。
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
JPLinkContainerView *linkView;
for (UIView *subview in self.subviews) {
if ([subview isKindOfClass:[JPLinkContainerView class]]) {
linkView = (JPLinkContainerView *)subview;
break;
}
}
CGPoint viewP = [self convertPoint:point toView:linkView];
if ([linkView pointInside:viewP withEvent:event]) { // 如果点击的点在联动视图linkView上, 就由linkView来响应事件
return [linkView hitTest:viewP withEvent:event];
}
else{
return [super hitTest:point withEvent:event]; // 否则, 执行系统默认的做法
}
}
04、使用注意点?
1.关于联动底部视图的高度
@property(nonatomic)CGFloat jp_linkViewHeight;
使用时注意 : 如果你下个界面需要有联动底部视图, 你在上个控制器 - (void)pushViewController:animated:
方法后面立即把值传给框架:
[self.navigationController pushViewController:YourOwnVC animated:YES];
YourOwnVC.navigationController.jp_linkViewHeight = YourHopeHeight;
同时注意 : 这两行代码有逻辑关系,必须先调用 push
方法,navigationController
才会 alloc
,分配内存地址,才有值。
2.关于联动底部视图
@property(nonatomic)UIView *jp_linkView;
你只需要在 viewDidLoad:
方法里把你的联动视图传给框架, 框架会制动帮你显示。
self.navigationController.jp_linkView = self.linkSubview;
05、这个框架用到的知识点?
- 这个框架中用到了很多
runtime
的知识,包括动态添加属性、动态添加方法、交换方法等,如果你对runtime知识感兴趣,欢迎阅读 1行代码快速集成按钮延时处理(hook实战)。 - demo中使用到了一个类似airbnb的头部视图层叠效果,如果你感兴趣,可以查看我以前的文章仿Airbnb的tableView头部视图层叠效果。
- 如果你对我之前实现自定义导航栏感兴趣,请点这里,自定义导航栏(设置每个界面的导航条透明度和颜色)。
06、Demo地址?
框架的Github地址在这里 JPNavigationController。如果我的文章在你实际工作中恰好帮到了你,请你给个小星星,谢谢。如果你和我一样热爱开源、热爱分享,或许可以小手一抖,帮我转发给更多朋友看到。
07、更新
2016.08.02:
有朋友在QQ上联系我说,他在实际开发中有需要在某个界面暂时关闭右滑手势的需求。所以,加入暂时关闭右滑手势开关,方便在某些情况下需要暂时关闭手势。具体更新见我的GitHub_Demo。2016.08.04:
上个版本使用类工厂方法,在类工厂方法里操作了用户传进来的控制器的View,所以会造成在应用启动的时候TabBarVC的子控制器一起加载,有性能问题.这次提交修复了这个问题, TabBarVC的子控制器都能遵循懒加载的原则.谢谢:袁小荣同学(Github)的提醒.2016.08.08:
这个框架有了较大的更新。具体包括给现有的Pop手势添加暂时关闭开关,以及自定义响应手势范围。在现有基础上,框架添加了底部联动视图,具体实现以及思路,请前往我的文章:1行代码为每个Controller自定义“TabBar”。2016.09.13
添加了直接拿到根导航控制器的接口,方便使用popToViewController功能。具体见demo第三个控制器的Pop。
08、注意
注意: tabBar 的 translucent 默认为 YES, 使用 JPNavigationCotroller 不能修改 tabBar 的透明属性. 这是因为 Xcode 9 以后, 苹果对导航控制器内部做了一些修改, 一旦将 tabBar 设为不透明, 当前架构下的 UI 就会错乱, 设置 tabBar 的 backgroundImage 为不透明图片, 或者设置 backgroundColor 为不透明的颜色值也是一样的会出错.
我的文章集合
下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。
我的文章集合索引
你还可以关注我自己维护的专题 iOS开发心得。这个专题的文章都是实打实的干货。如果你有问题,除了在文章最后留言,还可以在微博 @盼盼_HKbuy上给我留言,以及访问我的 Github。
赞助
你这一赞助,我写的就更来劲了!
微信赞助扫码
支付宝赞助扫码