引
这篇文章可以作为整个探秘系列的开端,后面会有很多短小精悍的小文章来探秘各种框架中的"未解之谜",从而可以让我们加深对iOS的理解,进而站在"巨人"的肩膀上创造出属于我们自己的Cocoa Project.
声明
这里本人不会写长篇大论,尽量保持一篇文章只讲述一个到两个点,这样我自己更容易梳理,大家也更容易理解.下面,精彩开始,我们的"开胃菜"就是常用上下拉刷新框架---MJRefresh.
开端
某一天,项目经理大大,突然给我布置了一个任务:我们涉及到的所有列表页都需要下拉可以加载最新数据,上拉可以加载更多数据.我当时就心想,这有何难,MJRefresh不就是为此量身定做的?于是拿出来三下五除二,搞定!然后去向经理大大邀功说已经完成了,经理大大也很震惊,没想到我这么快做好了,技术出身的他问我是怎么做的,我很理所当然的说出了是用了三方框架,然后他又问我:你知道是怎么实现这些功能的吗?然后......我懵逼了.那种囧态恐怕一生都不想记起了.
解密 - - mj_header 与 headerWithRefreshingBlock/headerWithRefreshingTarget的联系
通用姿势:
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
[self getListData];
}];
这里我有几个好奇的点:
- [MJRefreshNormalHeader headerWithRefreshingBlock]返回的是什么?
- mj_header是什么东东,tableView没有这个属性啊?
- 它俩到底是怎么关联的?如何拿到要监听的srollView?
各种command+左键查看之后发现如下几点:
- MJRefreshNormalHeader ->(表示继承于) MJRefreshStateHeader -> MJRefreshHeader -> MJRefreshComponent,有这么一个继承关系,而MJRefreshComponent的父类是UIView! 而headerWithRefreshingBlock返回的是一个instancetype类型,根据instancetype的特性,就是返回的是一个UIView的对象.
- mj_header是通过运行时为UIScrollView动态添加的属性,它的类型是MJRefreshHeader,根据上面的继承关系,也是一个view.那么这句代码就很好解释了
//这句代码返回了一个view,并且包含了执行刷新的方法
[MJRefreshNormalHeader headerWithRefreshingBlock:^{
[self getListData];
}];
//这句代码接收了返回的view,进一步理解就是给tableView添加了一个view,作为顶部显示刷新状态的header
self.tableView.mj_header = headerWithRefreshingBlock 返回的view;
- 了解以上两点之后,至于两者如何关联,我也是百思不得其解,后来进入mj_header的set方法以及MJRefreshComponent中发现了原因.
- 首先,mj_header的set方法处于UIScrollView的分类UIScrollView+MJRefresh.h中,这里有一句代码表示当前的ScrollView加入了上面2里的那个view:
- (void)setMj_header:(MJRefreshHeader *)mj_header
{
if (mj_header != self.mj_header) {
//其他代码略
//就是这一句代码,其实是将header插入到了当前的ScrollView中,这句代码很重要,会触发另一个方法.
[self insertSubview:mj_header atIndex:0];
//其他代码略
}
}
- 重点来了,因为我们的mj_header类型其实是MJRefreshHeader,而MJRefreshHeader的父类又是MJRefreshComponent,所以在MJRefreshComponent里我们发现了一个方法:
- (void)willMoveToSuperview:(UIView *)newSuperview{
};
这个方法我认为就是整个MJRefresh的核心,正是因为这个方法的特性,才使得后面对scrollView contentOffset的监听成为可能.
这里我们思考一个问题:MJRefresh采用的是KVO来监听scrollView的contentOffset,那么我们如何拿到这个scrollView呢?
答案就是willMoveToSuperview:(UIView )newSuperview方法.根据官方文档:
描述的意思是:当前的view(这里是mj_header)的父类改变成指定的类(scrollView)的时候,这个方法会触发.
参数描述中这个newSuperview就是我们要找的父类scrollView,因为上面mj_header的set方法中,已经将header添加到了scrollView中,insertSubview方法会触发willMoveToSuperview.
附代码:
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// 如果不是UIScrollView,不做任何事情
if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return;
// 旧的父控件移除监听
[self removeObservers];
if (newSuperview) { // 新的父控件
// 设置宽度
self.mj_w = newSuperview.mj_w;
// 设置位置
self.mj_x = 0;
// 记录UIScrollView
_scrollView = (UIScrollView *)newSuperview;
// 设置永远支持垂直弹簧效果
_scrollView.alwaysBounceVertical = YES;
// 记录UIScrollView最开始的contentInset
_scrollViewOriginalInset = _scrollView.contentInset;
// 添加监听
[self addObservers];
}
}
至此,scrollView已经被我们拿到了,接下来就是利用KVO对其属性进行监听了.
嗯,至此,今天也该告一段落了.至于关于监听的细节,其实是通过scrollView的inset和contentOffset来进行处理的,涉及到一系列逻辑判断,这里暂时不谈,因为在源码中有较多的注释可以供大家参考.后面我们会继续探索MJRefresh的框架设计-继承的经典使用!
网友评论