iOS平台,许多App的下拉刷新都是使用的MJRefresh这个开源库,笔者所接触开发过的几个App就是这样。当然还有很多效果酷炫的下拉刷新,真是不得不佩服开源界大神们的创意!这里是一些厉害框架的集合,你可以看看!

笔者是一个Android粉(ps:蒙谁呢,不就是舍不得肾嘛!),在使用起点读书这一App时,发现它所使用的下拉刷新十分简约,而且也能一定程度上吸引用户的注意,减少用户的心里等待时间。于是心血来潮,就想在iOS平台上实现这一效果。这次实践“捡起”了之前“丢掉”的东西,也学到了一些新东西,只是觉得遗憾的是在一些视觉设计上并没有达到模板的效果,文章后面会提到。这里先贴出一张效果图,如果你用过这个App,你应该就知道笔者简略了哪些地方:

我只是会转一圈而已

毫无疑问,这个刷新效果非常简洁,功能实现也相对简单。但是笔者在实现的过程中还是遇到了一些困境,比如原作中在下拉过程中strokeEnd逐渐增加的同时,笔画的头部有一个小箭头逐渐显现,最后完全显现,提醒用户此时结束滑动将能够触发刷新动作。这个很有意思的设计到笔者最终大致完成这个Demo的时候依旧没有什么头绪在iOS上实现,系统提供的lineCap并不能满足需求。还有就是原作中正在进行动画时,strokeEnd、strokeStart是以上次的结束点为起点,笔者进行了多番尝试,始终未果,所以在此虚心请教各位:如果有知道的朋友,请在评论区留下你的答案,谢谢!

为我所想要的而努力

正如标题中所说的那样(ps:笔者坚决不做标题党),本次实践的最终目的是实现一个简易的松耦合下拉刷新。何为松耦合?减少功能块之间的依赖程度,使其具有可移植性。比如说:你想让navigationBar的透明度随着视图的滚动而逐渐变化,于是乎你就在相关的滚动代理方法里实现了这样的目的。后面你慢慢发现随着项目的迭代,需要这样效果的界面越来越多,如果你有程序员的基本素养的话(DRY),你就会质疑自己的做法,因为做了太多的重复的体力工作了,而这就是代码之间高度耦合的表现。而解决这一问题可以使用的方法有很多,比如Method Swizzling。它的作用就是交换两个方法的实现体,是Objective-C实现AOP至关重要的黑魔法,Aspects就是为用户方便使用这一语法糖而诞生的框架。但就像念茜这篇文章里的总结所表现的那样:使用有风险,拌合需谨慎!

笔者在使用这一语法糖来完成本次实践的时候,踩了不少坑,大大增加了工作时间。具体是那些,后面将会一一叙述。在这里总结一下所用到的一些语法:CategoryProtocolMethod SwizzlingRuntime等,如果你是新手,对这些知识还比较陌生,建议先了解相关知识,这样有助于你理解后面文章中所贴出的代码。

如果你愿意一层一层一层的剥开我的心

从效果中可以很容易分辨出这一刷新效果的组成部分:圆形视图、动画展示刷新的进程。在下拉的过程中,UITableViewUISCrollViewcontentOffset并没有发生改变,初始状态能够正常的上滑。当滑动进程不够而结束滑动时,视图回到原点,即隐藏在navigationBar下面;在滑动到刷新过程停留的位置之外时,视图将动态回到停留处并开始动画。动画是连续的,直到刷新完成或者检测到当前网络连接中断(ps:Demo中只是模拟了网络连接的一个固定耗时,并没有做网络连接状态的监测,也就是在Demo中,刷新动作总是能够正常完成的)。刷新完成后,视图逐渐缩小最后消失,下次刷新时,圆形视图依旧从最上面滑动出现。

仅此而已的物语

正如著名小品《钟点工》里宋丹丹所讲的那样:“把大象关冰箱里总共分三步:第一步打开冰箱门,第二步把大象放冰箱里,第三步把冰箱门关上。”且不说第二步如何违背算法实现的可行性原则,只是这一个将具体问题分步骤解决的思想在编程过程中是到处都有所体现的,即面向过程编程。面向对象编程或是面向协议编程不外乎是对实现的过程进行了更深层次的抽象,像是以类的形式组织代码,以对象的形式封装数据等等思想都是以线性的思维过程为基石的,接下来就说明笔者完成Demo的几个步骤:

STEP1 决定图层的所属

从效果上看来很容易想到它是结合UIScrollViewUITableView使用的,因为刷新的一系列操作都有视图的滑动程度相关,因此可以让控制器实现相关的代理方法,并在其中插入对应的逻辑代码即可。从UITableViewtableHeaderView属性中可以获得灵感,即为UIScrollView动态绑定一个属性(ps:UITableView继承自UIScrollView),这里姑且把它命名为ZNHeader。由于这一效果不适用于上拉刷新,故笔者就没有去捣鼓了。我们都知道Category是不能添加属性的,但可以通过Runtime来变相为类添加属性,下面贴出主要代码:

1
2
3
4
5
6
7
8
9
10
#import <objc/runtime.h>
//....
- (ZNRefreshHUD *)ZNHeader {
return objc_getAssociatedObject(self, (__bridge void *)@"ZNHeader");
}

- (void)setZNHeader:(ZNRefreshHUD *)ZNHeader {
//.....
objc_setAssociatedObject(self, ((__bridge void *)@"ZNHeader"), ZNHeader, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

这几个运行时函数功能很直观,一个是返回对象的对应属性名的实例,一个是指定为哪个对象绑定什么对象到什么属性关键字并指定动态关联策略。这种做法很容易和在类的接口中显式声明属性相互对应起来,应该不难理解。

STEP2 描述图层的行为

笔者将整个图层描述为ZNRefreshHUD: CAShapeLayer的实例(实现了CAAnimationDelegate),并用两个UIBezierPath实例表征它的绘画PathshadowPath。在它滑动或回滚到预制的停留点时,就让它关联一个CAAnimationGroup对象,其中包含了旋转、绘画开始、绘画结束三种动画。通过动画代理,在动画开始时执行遵循了回调代理协议的代理对象的刷新逻辑,Demo中用了一个延时模拟网络加载,并通过获取最新系统时间来代表数据的刷新与获取。当刷新任务完成时,通过ZNRefreshHUD的一个实例方法finishRefreshing结束动画。下面是这个方法里的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)finishRefreshing {
[self removeAllAnimations];

[CATransaction begin];
[CATransaction setCompletionBlock:^{
self.strokeEnd = 0.0f;
self.strokeStart = 0.0f;
self.strokeColor = [[UIColor colorWithCGColor:self.strokeColor] colorWithAlphaComponent:0.3f].CGColor;
}];

[CATransaction setAnimationDuration:0.3f];
self.transform = CATransform3DScale(self.transform, 0.01f, 0.01f, 0.01f);
[CATransaction commit];
}

UIView的动画接口其实是CATransaction这专门用于图层动画行为的更上一层级的抽象,不过在使用这一接口时需要注意setCompletionBlock的调用时机。当时笔者是在设置了动画的执行块后调用的,结果发现系统就直接执行完成快里的代码而忽略了动画快。在StackOverflow上查找了相关的问题,上面给出的答案正如上面的代码一样,只要在设定实际的动画逻辑前设置就可以了。(ps:不知道这算不算BUG)

STEP3 让图层可交互

可交互就是让图层能够感知用户的行为来决定当前的刷新进程,给予用户的感觉就是自己能够自由控制刷新是否需要进行下去。为了做到松耦合,那么就不能再图层的拥有者里实现交互逻辑。笔者最初想到的就是使用Method Swizzling,可能是因为平时使用不够熟练,在这过程中走了一些弯路,好在最终达到了终点。

起初,笔者添加了CAShapeLayer分类,用自定义的方法替换了控制器中的scrollViewWillBeginDragging:、scrollViewDidScroll:、scrollViewDidEndDragging:willDecelerate:实现逻辑。由于涉及到两个类之间的方法拌合,如果你要使用self.xxx,就要保证需要操作的对象必须是这两个类的共性。又因为最初笔者是通过为视图添加一个额外的图层来实现功能的,导致笔者不得不写一个单例的类构造方法出来实现统一。

嗯,感觉还不错,编译运行!你丫!!!野指针错误!等等,先让控制器实现这三个代理方法!OK,功能正常。“喂,大兄弟,你这样也太不友好了嘛,我明明什么都不做,还让我写三个空方法,看着让人X疼啊!”,笔者这样设身处地地为别人考量着。分析一下代码流程:我要先拿到控制器原有方法实现体和自实现方法体然后再来拌合,那么当控制器没有显示给出实现体时,系统的默认调用的实现体是存在的吗?默默地NSLog一下,结果发现它根本不允许访问,这就难怪野指针了。那么我用Block自实现方法体来拌合总没问题了吧!Bingo,you are right!

嘿嘿,可以差不多交差了!额,怪了,那么这样我岂不是与控制器强耦合了嘛,真正的生死与共了啊!要不得要不得,别人岂不是就只有叫这个名字的控制器才能使用,因为其中的方法拌合所取出的方法相关类是这一个控制器的类名。要完蛋要完蛋,改改改!好好想想,如果让UIScrollView绑定一个关于下拉刷新视图的属性,当控制器拥有UIScrollView、UITableView属性实例时,就能够以self.tableView.xxx、self.scrollView.xxx这样的形式来访问下拉刷新的图层,那么不同类的在这方面的差异性就消除了。通常情况下,UIScrollView、UITableView的拥有者都可以是UITableViewController,因为这样可以更方便的访问到表视图,所以为什么不在UITableViewController中实现Method Swizzling?很清楚的是,这一做法能够解决之前所遇到的一切问题!于是,笔者兴致勃勃的完成了改进,结果发现当自实现了这三个方法时,所拌合的方法逻辑就将得不到明确的体现!想了一下发现,因为笔者拌合是父类的实现,如果自实现了,父类的方法体就会被覆盖(ps:这里是指用户额外添加的逻辑,而非系统级的核心功能),所以就需要在自实现的方法体里调用父类的实现,这就很像控制器的视图生命周期的各个方法都需要调用父类实现这一行为。就像下面这样:

1
2
3
4
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
[super scrollViewWillBeginDragging:scrollView];
}
//....

总结

这个Demo前前后后大概花了两周的时间,主要大三下了课业繁忙加上学驾校的耗时,好在最后完成了,不然脸就要被自己打肿了!这样一个过程还是让自己学到了一些新东西,加深了某些知识点的理解。笔者觉得这样的一个学习方式比起盲目的找各种博客来看来学让人记忆更深刻,毕竟这是你自己给自己定下的任务,而且是在你能力范围之内的。查查资料,好好思考,努力完成它,又何乐而不为了!(ps:不就是因为实验室里没实际项目练手,觉得无趣发发牢骚嘛!)这里可以找到完整代码,有兴趣的可以拉下来把玩把玩,谢谢!

后续

这是关于presentationLayer、modelLayer的知识简要说明,当时笔者做了相关标记要在写这篇文章时提到,但最终还是莫名其妙的忘了,真是抱歉!

其实在笔者转载的文章里有非常详尽的介绍,但是考虑到文章篇幅过长以致于各位看官不容易查找到有关内容,于是就近的给出点解释,希望能解些疑窦。

一个CALayer的实例都具有一个presentationLayer的属性,可以将它理解为呈现或者表现图层。实例的每个属性值都存放在这个图层当中,这个呈现图层实际是实际的模型图层的拷贝,但是它代表了任何指定时刻当前图层外观效果的实际属性值。当你给实际模型图层施加动画效果,CoreAnimation就会将这些属性值的变化交给呈现图层来展示,同时它还将用来处理用户交互。这就是为什么默认进行动画后,如果要重复进行,又得从之前的起始位置开始。我们也可以让图层保留动画后的效果,它本质上就是在动画完成后更新模型图层的位置与呈现视图一致。