DWCollectionView

作者: Dawn_wdf | 来源:发表于2017-07-28 15:31 被阅读69次

    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的扩展类

    相关文章

      网友评论

        本文标题:DWCollectionView

        本文链接:https://www.haomeiwen.com/subject/uonxlxtx.html