首先附上项目地址:
FDFullscreenPopGesture
本篇使用1.1版本源码进行学习
此项目以AOP的方式实现了UINavigationController的“一行代码实现全屏滑动返回”功能。其主要功能是通过UINavigationController内置的interactivePopGestureRecognizer手势对象。通过使用自定义的UIPanGestureRecognizer类实例绑定原手势的action及SEL,使响应范围扩大到整个屏幕,巧妙地实现了此功能。详细解释可以查看原博客轻松学习之二——iOS利用Runtime自定义控制器POP手势动画或本人的读后解析UINavigationController的全屏拖动返回
。
1. 主要结构说明
整个项目非常简单,对外只有一组.h及.m文件。代码量也只有区区两三百行,足够轻量且高效,值得我等膜拜学习~
1.1 头文件的结构说明
项目对外公开的类及主要功能为:
- UINavigationController的扩展类:UINavigationController + FDFullscreenPopGesture
名称 | 类型 | 说明 |
---|---|---|
fd_fullscreenPopGestureRecognizer | 属性 | 真正的全屏拖动响应的手势对象 |
fd_viewControllerBasedNavigationBarAppearanceEnabled | 属性 | 是否允许ViewController对象自定义导航栏外观(默认为YES) |
- UIViewController的扩展类:UIViewController + FDFullscreenPopGesture
主要用于ViewController对象自主控制全屏手势的使能(以便ViewController的视图也处理自定义的拖动手势,防止手势冲突)
名称 | 类型 | 说明 |
---|---|---|
fd_interactivePopDisabled | 属性 | 是否允许响应UINavigationController的全屏拖动手势 |
fd_prefersNavigationBarHidden | 属性 | 表名当前ViewController对象的导航栏是否隐藏(默认为NO) |
1.2 实现文件的结构说明
- 遵循UIGestureRecognizerDelegate协议的类:_FDFullscreenPopGestureRecognizerDelegate
名称 | 类型 | 说明 |
---|---|---|
navigationController | weak属性 | 内部需要根据导航器对象做处理 |
内部实现了gestureRecognizerShouldBegin:方法,用于处理不同情况下是否响应拖动手势。
设计此类的主要目的是将此功能独立出来,作为UINavigationController + FDFullscreenPopGesture的工具类使用,简化了原类代码,使结构更加清晰。
- UIViewController的私有扩展类:UIViewController + FDFullscreenPopGesturePrivate
名称 | 类型 | 说明 |
---|---|---|
fd_willAppearInjectBlock | block对象,属性 | 保存注入的block对象到堆内存,等待合适时机执行【按命名来说,在ViewWillAppear方法中执行】 |
在运行时交换了ViewWillAppear:方法实现,在其中注入执行了block对象。
- UINavigationController + FDFullscreenPopGesture的实现
交换了pushViewController:animated:方法实现,在内部实现了主要功能:
- 使用自定义的UIPanGestureRecognizer对象绑定了原有手势的action和SEL,添加到UINavigationController的切换控制视图上。
- 声明并实现了注入的_FDViewControllerWillAppearInjectBlock对象,并赋值给指定的UIViewController对象。
- UIViewController + FDFullscreenPopGesture的实现
只是对扩展中声明的属性进行实现。
2. 使用Category扩展原类,并向指定方法注入新的功能
在OC中,一般是通过Category对Class进行扩展的方式来实现AOP的。本项目即是如此。例如,通过对UINavigationController进行扩展,在load时对方法进行替换,将手势添加到导航器控制视图上,并绑定上原来的target和SEL:
+ (void)load
{
// 交换push方法实现,用于执行自定义操作
Method originalMethod = class_getInstanceMethod(self, @selector(pushViewController:animated:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(fd_pushViewController:animated:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)fd_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
// 检查导航器的切换视图中的手势数组中,是否已经包含全屏拖动手势对象
if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {
// 没有,则向其中添加自定义的拖拽手势
[self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];
// 通过查看运行时期间,手势对象内部的存储结构,获取原手势响应的target和执行的SEL
NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
// 设置自定义的拖拽手势代理,处理不同情况下手势是否响应(故上面的类用于解耦手势的代理方法实现,简化本类代码)
self.fd_fullscreenPopGestureRecognizer.delegate = self.fd_popGestureRecognizerDelegate;
// 将原手势的action和SEL绑定到自定义的拖拽手势中
[self.fd_fullscreenPopGestureRecognizer addTarget:internalTarget action:internalAction];
// 禁止原手势响应(原手势为边缘拖动手势)
self.interactivePopGestureRecognizer.enabled = NO;
}
// 处理自定义导航栏外观
[self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];
[self fd_pushViewController:viewController animated:animated];
}
如此实现,即可在引入头文件后,对应的UINavigationController实例自动添加全屏返回手势,对原类的耦合性降至最低。
本项目中,UIViewController的扩展也使用了此方式,以达到在ViewWillAppear:方法中执行注入block,实现运行时对导航栏隐藏设置的修改。充分利用了OC语言的动态性,使代码简洁易用,降低耦合。
3. 防止block对self捕获产生的引用循环,并保证block执行时的完整性
在fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:方法中,作者初始化了UIViewController对象在ViewWillAppear:方法中需要注入的block。先看代码:
- (void)fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:(UIViewController *)appearingViewController
{
// 检查ViewController对象是否允许自定义导航栏外观
if (!self.fd_viewControllerBasedNavigationBarAppearanceEnabled) {
return;
}
// 声明self弱指针
__weak typeof(self) weakSelf = self;
// 声明并实现注入的block对象
_FDViewControllerWillAppearInjectBlock block = ^(UIViewController *viewController, BOOL animated) {
// block内部捕获的是self的弱指针,不会对self进行强引用
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
// 根据外部设置,隐藏或显示当前ViewController下的导航栏
[strongSelf setNavigationBarHidden:viewController.fd_prefersNavigationBarHidden animated:animated];
}
};
// 将block对象赋值给扩展后的ViewController对象(适时调用)
appearingViewController.fd_willAppearInjectBlock = block;
// 获取导航栈的最后一个ViewController(即当前已显示的,马上要被新的ViewController压到栈中的)
UIViewController *disappearingViewController = self.viewControllers.lastObject;
if (disappearingViewController && !disappearingViewController.fd_willAppearInjectBlock) {
// 即将消失的ViewController若存在(即新push的ViewController不是根视图的情况),也将block赋值给它(以备pop时,前一个ViewController对象也能执行block,viewWillAppear时修改导航栏外观)
disappearingViewController.fd_willAppearInjectBlock = block;
}
}
其中,最为重要的是:
// block外部声明
__weak typeof(self) weakSelf = self;
// block函数体内部声明
__strong typeof(weakSelf) strongSelf = weakSelf;
我们都知道,使用__weak修饰的OC对象,可以防止__NSMallocBlock对象在捕获时对其添加强引用(详见《Objective-C高级编程 iOS与OS X多线程与内存管理》16),避免产生引用循环。但是这样有一个弊端,就是假设当self释放后,若block在此时执行,self对象自动置为nil,预定的功能可能就无法正常实现了。为了防止这种情况发生,可以声明一个__strong修饰的局部指针变量,指向捕获的弱对象,既可以保证在block执行过程中self不被释放,还可以避免对self进行强引用(__strong变量会在作用域外被自动release)。
4. 总结
- FDFullscreenPopGesture主要通过Category的方式对UINavigationController和UIViewController进行功能扩展,避免了Class继承方式的“笨重”,简化了引入和使用。
- 在Category的load方法中,替换了原类的方法实现(原类的load加载要在扩展类之前),在其中注入了指定功能。
- 使用KVC的方式,巧妙地进行UIGestureRecognizer对象的替换,近乎“无损”地实现了全屏滑动返回功能,避免了使用传统的交互式动画方式实现自定义的UINavigationController。
- 使用__weak对象避免block的引用循环,并用__strong对象保证block执行过程中捕获对象不被释放。
网友评论