github传送门:https://github.com/DawnWdf/DWCollectionView
支持Carthage安装,请在Cartfile中填写
github "DawnWdf/DWCollectionView"
支持cocoaPods安装
pod "DWCollectionView"
为什么封装CollectionView
项目中用到collectionView的地方很多,每一个VC里面都需要至少两个代理方法,如果碰到页面稍微复杂一点,代理方法写的更多。当有N个页面都需要写相同的N个代理方法的时候。。。。。。
页面需要多个collectionView,需要在多个代理方法中判断当前使用的collectionView是哪个
-
一个collectionView需要使用很多个不同样子的cell。定义了N个cell,于是在每一个代理方法里面都有一个if-else来各种判断。
当然,这个可以通过其他的方法来规避部分if-else判断。比如说:当ModelA对应CellA,ModelB对应CellB……
- 让所有的Model都遵循一个协议ModelPropocol。让所有的cell都继承自一个基类BaseCell。
- ModelPropocol有一个方法cellNameFor,根据不用的ID来返回对应的cell的类名
- 在cellForItemAtIndexPath代理方法里面根据cell的类名创建cell,并执行数据绑定的操作。
但是这个做法也有很多弊端,比如
- 在使用例如didSelectItemAtIndexPath方法时依然要if-else,使用numberOfItemsInSection方法时也依然要判断。
- 同时也需要定义很多个id用来在相同model的情况下区分不同的cell。
- 业务要是再复杂一点,感觉就像是将每一个代理方法里面的if-else分发到了model中一样。model过于沉重,不仅保存了数据,还保存了对应的UI,还需要针对每一个代理方法做多余的操作。感觉已经超出了重量级model该做的事情。
- 我们希望我们的model或者cell可以复用,当我们希望某一个model可以对接不同页面不同cell的时候。。。。。。又或者希望我们的model和cell是可插拔式的。
有的项目中会有一些比较复杂和灵活的页面。比如,整个页面都是可以自由配置的。需要根据接口返回的数据来进行排版布局。像是我现在做的项目,除了要根据返回的数据来布局模块的顺序,还要求配置页面上两个cell之间是否有一个10像素的间距,配置某一个cell上面或者下面是否有一个1像素的分割线。如果接口返回的数据结构正好可以对接你的UI,那真是可喜可贺,如果无法对接,需要自己判断和组装然后再渲染视图。等你渲染了视图,接口要是升级或者字段调整。。。。。。万一架构的时候脑抽,或者写代码的时候犯二,那真是“完美”。当然如果架构够好,这也是没什么的。
我相信一定会有人做过类似的项目,踩过类似的坑的,对很多类似的、机械似的代码表示厌烦。于是我封装了collectionView。当然我不会告诉你,同组的一个大神封装了一个tableview让我受益匪浅,燃起了自己也写一个的欲望。这充分说明了,跟着大神走,有肉吃。
封装后可以渲染哪种页面
-
普通列表
-
用户中心
-
瀑布流(配合使用flowLayout)
还有多种多样列表
只要配置好model与cell的对应关系,只要管理数据结构就可以渲染视图了。
代码如何实现
- 创建collection
就像创建一个普通UICollectionView一样,除了把类名换成DWCollectionView以外,没有其他操作。而且在不声明UICollectionViewLayout的情况下,默认添加上去,免得崩溃。
DWCollectionView *cv= [[DWCollectionView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame)) collectionViewLayout:layout];
cv.backgroundColor = [UIColor whiteColor];
cv.delegate = self;
[self.view addSubview:cv];
- 配置model和cell的关系
- 创建collectionView的时候我并没有配置dataSource = self;也没有给collectionView注册任何cell或者reuseview。
- 然而我们的collectionView需要配置数据源,并必须实现协议UICollectionViewDataSource中两个方法。
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
- 通常情况下cellForItemAtIndexPath的方法里面会写model和cell的对应关系,通过数据源和IndexPath找到model,再通过model去创建或者复用cell,然后给cell进行数据绑定。这个方法大概是所有代理方法中最重的一个。
- 我封装后的collectionView则将注册和model&cell之间的绑定简化了一下。
[self.collectionView registerViewAndModel:^(DWCollectionDelegateMaker *maker) {
maker.registerCell([TeamInfoCell class],[TeamInfo class])
.itemSize(^(NSIndexPath *indexPath, id data){
return CGSizeMake(100, 140);
})
.adapter(^(UICollectionViewCell *cell, NSIndexPath *indexPath, id data){
TeamInfoCell *newCell = (TeamInfoCell *)cell;
newCell.showImage = YES;
[newCell bindData:data];
})
.didSelect(^(NSIndexPath *indexPath, id data){
NSLog(@"did select block : 如果vc中实现了didSelect的代理方法,则在此block后执行");
});
}];
-
整体采用响应链式的编程方式。
registerViewAndModel方法承担了cellForItemAtIndexPath全部的工作。
maker.registerCell
的工作是告诉collectionView将model和cell绑定,只要数据源中出现model,就用对应的cell去渲染视图。
maker. itemSize
替代了- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;
maker. adapter
中的block则返回了每一个cell和当前cell对应的具体的数据,在这里我们可以进行数据绑定,将model中具体的内容渲染到cell中。这样就节省了通过数据源和Indexpath来找到对应model再去渲染的麻烦。这里的cell我都遵循了
DWCollectionViewCellProtocol
协议,实现了- (void)bindData:(id)data;
方法,以便在cell中做具体的绑定操作。maker.didSelect
的方法则完全是代理方法- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
的替代。
但是不是完全的替代,如果当前的VC中同时实现了这个代理方法,那么block中的方法先执行,然后执行代理中的方法。
到这里,一个简单的collectionView已经搭建完毕。这里通过几个block完成了注册视图和至少三个必备代理方法。
我们再也不用满VC去找每个代理方法然后做处理了。因为都在这了。
类似的,header&footer的方法一致。
//header
maker.registerHeader([UserCenterHeaderCollectionReusableView class],[UserCenterHeaderModel class])
.sizeConfiger(^(UICollectionViewLayout *layout,NSInteger section, id data){
return CGSizeMake(screenW, 33);
})
.adapter(^(UICollectionReusableView *reusableView,NSIndexPath *indexPath, id data) {
UserCenterHeaderCollectionReusableView *view = (UserCenterHeaderCollectionReusableView *)reusableView;
[view bindData:data];
});
//footer
maker.registerFooter([UICollectionReusableView class],[NSString class])
.sizeConfiger(^(UICollectionViewLayout *layout,NSInteger section, id data){
return CGSizeMake(screenW, 10);
})
.adapter(^(UICollectionReusableView *reusableView,NSIndexPath *indexPath, id data) {
reusableView.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.5];
});
-
给collectionView赋值。
由于封装后的collectionView是数据驱动视图的,给collectionView赋值就变得很重要。所以数据也进行了一次封装。
按照collectionView每一个section分为头、尾和items将数据分为三个对应的部分@interface DWSection : NSObject <NSCoding> @property (nonatomic, strong) id headerData; @property (nonatomic, strong) id footerData; @property (nonatomic, strong) NSArray *items; @end
items里面存的就是所有需要展示的,并且已经注册过的model。
[self.collectionView setData:data];
data为一个数组,里面的每一个元素都是DWSection的对象。
这样做有一个好处就是,这个model很大的作用其实是脱离业务的,针对视图的model。这样如果接口数据结构有变化,而UI无变化,只要将接口数据和model做对接就可以了。而且model可插拔,如果这个cell&model要移植到其他的项目或功能中,也只要在拼接数据的时候做点手脚就可以。举个栗子:在做项目的时候,接口数据不能及时给出,就需要客户端做一个假的数据,我就根据接口文档写了dictionary来渲染cell。但是当接入了接口,使用工程基本网络框架后发现,它自动把返回的字典转成了对应的业务相关model,里面一大堆跟UI无关的数据。于是我直接从业务model中抽出UI需要展示的属性直接赋值给model&cell。
我在实际项目中使用的时候,这个model大多数都有几个相同的属性
@property (nonatomic, copy) NSString *title;//cell标题 @property (nonatomic, copy) NSString *imageUrl;//图片 @property (nonatomic, copy) NSString *content;//内容 @property (nonatomic, copy) NSString *scheme;//跳转URL
主要说一下属性scheme。有一段时间router这个东西特别流行,我想现在应该有很多项目也都有使用router。而这个scheme就是为了router而存在的。我们的cell在点击的时候大多要跳转到一个二级页面,有时需要传递一些参数,id/type什么的。之前的做法则是在model中也声明一个属性ID,然后跳转的时候传值。
这里我们可以在viewModel中做数据转换的时候,就根据要求将scheme拼接好,将需要传递的参数都放在sheme中。这样点击cell进行页面跳转的时候可以统一使用scheme进行页面跳转,很大程度上降低了耦合度。如果所有的model的scheme属性都一样的话,就更加快捷,我们都不用关心我们拿到的id类型的数据data到底是哪个model了,只要它实现了scheme就行。像这样if ([data respondsToSelector:NSSelectorFromString(@"scheme")]) { SEL sel = NSSelectorFromString(@"scheme"); IMP selImp = [data methodForSelector:sel]; id(*func)(id,SEL) = (void *)selImp; id scheme = func(data,sel); [ARouter jumpWithScheme:scheme title:nil other:nil]; }
到此,一个简单的collectionView就全部完毕。总结一下就三个步骤:
- 创建
- 绑定
- 添加数据源
其他的代理方法
我在封装的时候,希望对原有collectionView的侵入性最小。所以你会发现,只有上面提到的常用的代理方法是使用block的形式封装在了一起。如果collectionView的功能比较多,需要实现其他的代理方法,比如:- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath ;
还是需要自己在vc中写好的。
所以,如果对layout有特殊的要求,依然可以实现相应的代理方法。如:
#pragma mark - UICollectionViewDelegateFlowLayout
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(10, 10, 10, 10);
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 10;
}
或者在创建的时候直接使用
UICollectionViewFlowLayout *sysLayout = [[UICollectionViewFlowLayout alloc] init];
sysLayout.estimatedItemSize = CGSizeMake(100, 150);
sysLayout.minimumLineSpacing = 50;
这些都是使用UICollectionView标准的方式,不赘述。
题外话:DWFlowLayout(自定义布局-瀑布流),不在封装范围内。
缺憾
为了能更好的封装代理方法,我将代理做了重定向。所以,collectionView的代理在执行的时候用的并不是声明时指向的代理VC,而是我自己的代理类DWCollectionDelegate。我只是在DWCollectionDelegate记录了VC,并在对应的方法中调用了一次VC的方法。
然而collectionView遵循的协议有四个:
- UICollectionViewDataSource
- UICollectionViewDelegate
- UICollectionViewDelegateFlowLayout
- UIScrollViewDelegate
如果把所有的协议的代理方法都写出来,那就是个天文数字。
所以我将不常用的代理方法使用runtime的方法做了转换。
具体的代码如下:
for (int i = 0; i < protocolMethodCount; i++) {
struct objc_method_description protocolObject = protocolDes[i];
SEL selector = protocolObject.name;
//originalDelegate是否实现此方法
BOOL isOriginalResponse = class_respondsToSelector(original , selector);
if (isOriginalResponse) {
Method originalMethod = class_getInstanceMethod(original, selector);
class_replaceMethod(aclass, selector, class_getMethodImplementation(original, selector), method_getTypeEncoding(originalMethod));
}
}
注释中的originalDelegate代表的就是声明时设置的代理VC。‘当前类’代表的就是DWCollectionDelegate。从代码中可以看出我实际上是将两个类的代理方法的imp互换了。所以就会出现一个问题,例如代理方法-(void)scrollViewDidScroll:(UIScrollView *)scrollView
在当前类中不存在,但是在originalDelegate中存在了,替换了imp后,在scrollViewDidScroll方法中的self就成了DWCollectionDelegate。所以这样的代理方法中就要判断一下self是哪个类。同时,由于DWCollectionDelegate已经添加了这个代理方法,如果在其他的VC中不需要执行这个代理方法,它会去实现过的方法中找,所以也需要判断delegate.originalDelegate是否为你需要的vc。逊毙了!!!low货!!
UserCenterViewController.m中实现代理方法
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint offset = scrollView.contentOffset;
if ([self isKindOfClass:[DWCollectionDelegate class]]) {
DWCollectionDelegate *delegate = (DWCollectionDelegate *)self;
id original = delegate.originalDelegate;
if ([original isKindOfClass:[UserCenterViewController class]]) {
UserCenterViewController *vc = (UserCenterViewController *)original;
[vc updateUserInforView:scrollView];
[vc updateNav:offset];
}
}else if([self isKindOfClass:[UserCenterViewController class]]){
[self updateUserInforView:scrollView];
[self updateNav:offset];
}
}
为了方便,我将这部分判断做成了宏定义
#define DW_CheckSelfClass(calssName) \
calssName *trueSelf = self; \
if ([self isKindOfClass:[DWCollectionDelegate class]]) { \
DWCollectionDelegate *delegate = (DWCollectionDelegate *)self; \
id original = delegate.originalDelegate; \
if ([original isKindOfClass:[calssName class]]) { \
calssName *vc = (calssName *)original; \
trueSelf = vc;\
}else{ \
return; \
} \
}else if([self isKindOfClass:[calssName class]]){ \
trueSelf = self; \
} \
\
所以,新的代码为
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint offset = scrollView.contentOffset;
DW_CheckSelfClass(UserCenterViewController);
[trueSelf updateUserInforView:scrollView];
[trueSelf updateNav:offset];
}
这个问题暂时还没有找到解决的方案。如果有大神出手,我会更新。如果路过的大神有方法,还请路见不平一声吼。多谢!
在此列出已经在DWCollectionDelegate实现的代理方法,在以下方法中可以不去判断self的类型。
- UICollectionViewDelegateFlowLayout
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section
- UICollectionViewDelegate
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
- UICollectionViewDataSource
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView //不需要实现
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section //不需要实现
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath//不需要实现
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
封装思路
- 代理方法重定向到DWCollectionViewDelegate,并保存原始代理类对象
- 使用字典保存所有信息,包括注册的cell和model的类名、block等
- 获取所有代理方法,并将必要的代理方法做imp指向
- 在DWCollectionViewDelegate对应的代理方法中取得字典中保存的数据做block,或者调用原始类对象的代理
PS
暴露两个NSObject的扩展类
- NSObject+Coding : 最快速度让一个NSObject类支持coding
http://www.jianshu.com/p/7e117a9fb2bd - NSObject+MulArgPerformSel : 让某对象执行特定多参数方法
http://www.jianshu.com/p/f228a40e10a9