DWCollectionView

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让我受益匪浅,燃起了自己也写一个的欲望。这充分说明了,跟着大神走,有肉吃。

封装后可以渲染哪种页面

  • 普通列表


    Simulator Screen Shot - iPhone 8 Plus - 2017-11-24 at 18.01.49.png
  • 用户中心


    Simulator Screen Shot - iPhone 8 Plus - 2017-11-27 at 11.13.51.png
  • 瀑布流(配合使用flowLayout)
    Simulator Screen Shot - iPhone 8 Plus - 2017-11-27 at 11.34.21.png
  • 还有多种多样列表

Simulator Screen Shot - iPhone 8 Plus - 2017-11-27 at 11.39.06.png
Simulator Screen Shot - iPhone 8 Plus - 2017-11-27 at 11.39.14.png

只要配置好model与cell的对应关系,只要管理数据结构就可以渲染视图了。

代码如何实现

  1. 创建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];
  1. 配置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就全部完毕。总结一下就三个步骤:

  1. 创建
  2. 绑定
  3. 添加数据源

其他的代理方法

我在封装的时候,希望对原有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的扩展类

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,165评论 1 23
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,029评论 4 62
  • 【销售八大方法】 1、最基本的销售方法:卖文化、卖自己、卖产品 2、最有效的销售方法:事实与数据 3、最持续的销售...
    连佩丽阅读 363评论 0 0
  • 渐渐讨厌一个人的感觉,就像是针管慢慢刺入皮肤,由紧张到适应,刺痛到麻木,直到针被抽出,冒出殷红的血,淡淡的血...
    ZSSXH阅读 207评论 0 0