美文网首页
iOS 一种组件化的Collection View实现

iOS 一种组件化的Collection View实现

作者: Leemmin | 来源:发表于2019-01-07 17:55 被阅读0次

    参考文章: 一种组件化的Table View实现

    背景

    之前一直使用UITableView且cell类型单一,尚且看不出来系统原生写法的缺陷。这次使用UICollectionView,各个section、cell的代理属性越来越多,恰好遇上多个类型的cell,这样在每个Delegate或DataSource中需要大量使用switch语句,使得代码十分臃肿,VC极速膨胀,极难维护,而且这时如果想要再加入一种新类型cell,那就要修改所有的Delegate和DataSource方法,非常麻烦。参考一位大佬TableView组件化的实现,对Collection View做了组件化的实现,大大缩减VC代码量,且使得代码容易维护,容易修改。

    组件(Component)

    在MVC的架构中,没有viewmodel的概念,使得大量本不该出现在VC里的逻辑使VC变得非常膨胀。组件就是MVVM架构中的viewmodel。
    通过定义UICollectionView中的一个section作为一个组件,管理自己的cells,size,supplementaryView等等任何出现在 UICollectionViewDataSourceUICollectionViewDelegateUICollectionViewDelegateFlowLayout 中的代理方法。

    //LLCollectionComponent.h
    
    @protocol LLCollectionComponent <NSObject>
    
    @required
    
    //cell及header的标识符,用于复用
    - (NSString *)cellIdentifier;
    - (NSString *)headerIdentifier;
    
    //方便子类注册自己的cell
    - (void)registerWithCollectionView:(__kindof UITableView *)collectionView;
    
    - (NSUInteger)numberOfItems;
    
    - (CGSize)sizeForComponentItemAtIndex:(NSUInteger)index;
    
    - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
    
    
    @optional
    
    - (__kindof UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;
    
    - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;
    
    @end
    

    - (void)registerWithCollectionView:(__kindof UITableView *)collectionView;方法可以让你方便的在子类注册自己的cell、supplementaryView

    以上是最简单的UICollectionView应实现的或是最常用的代理方法,可以实现一个BaseComponent作为你的项目中的基础组件,可以在其中扩展一些你的项目所需的属性:

    //LLBaseComponent.h
    
    typedef NS_ENUM(NSUInteger, LLComponentType) {
        LLComponentTypeNone = 0,
        LLComponentTypeTop = 1,
    };
    
    @class LLDiscoverRecommendData;
    @interface LLBaseComponent : NSObject <LLCollectionComponent>
    
    @property (nonatomic, strong) LLDiscoverRecommendData *data;
    @property (nonatomic, weak) id<LLCollectionComponentDelegate> delegate;
    @property (nonatomic, weak, readonly) UICollectionView *collectionView;
    
    @property (nonatomic, copy) NSString *cellIdentifier;
    @property (nonatomic, copy) NSString *headerIdentifier;
    
    @property (nonatomic, assign) CGFloat horizontalMargin;
    @property (nonatomic, assign) CGFloat minimumLineSpacing;
    @property (nonatomic, assign) CGFloat minimumInteritemSpacing;
    @property (nonatomic, assign) UIEdgeInsets inset;
    
    //根据请求返回的model来实例化
    + (instancetype)componentWithCollectionView:(UICollectionView *)collectionView data:(nonnull LLDiscoverRecommendData *)data;
    + (instancetype)componentWithCollectionView:(UICollectionView *)collectionView data:(nonnull LLDiscoverRecommendData *)data delegate:(nullable id<LLCollectionComponentDelegate>)delegate;
    
    //根据类型来实例化。目前支持顶部component
    + (instancetype)componentWithCollectionView:(UICollectionView *)collectionView type:(LLComponentType)type;
    + (instancetype)componentWithCollectionView:(UICollectionView *)collectionView type:(LLComponentType)type delegate:(nullable id<LLCollectionComponentDelegate>)delegate;
    
    - (instancetype)init NS_UNAVAILABLE;
    
    - (void)registerWithCollectionView:(__kindof UICollectionView *)collectionView NS_REQUIRES_SUPER;
    
    @end
    

    在基础组件中初始化这些属性为0或空或默认值,cell、header复用标识符在基类初始化后子类便无需实现了。

    @implementation LLBaseComponent
    
    #pragma mark - class method
    
    //根据请求返回的model来实例化
    + (instancetype)componentWithCollectionView:(UICollectionView *)collectionView data:(LLDiscoverRecommendData *)data {
        return [self componentWithCollectionView:collectionView data:data delegate:nil];
    }
    
    + (instancetype)componentWithCollectionView:(UICollectionView *)collectionView data:(LLDiscoverRecommendData *)data delegate:(id<LLCollectionComponentDelegate>)delegate {
        Class componentClass = nil;
        switch (data.showType) {
            //根据type来返回相应的子类
            ···
            default:
                break;
        }
        if (componentClass) {
            return [[componentClass alloc] initWithCollectionView:collectionView data:data delegate:delegate];
        }
        return nil;
    }
    
    //不由model初始化,根据类型来初始化,目前支持顶部component
    + (instancetype)componentWithCollectionView:(UICollectionView *)collectionView type:(LLComponentType)type {
        return [self componentWithCollectionView:collectionView type:type delegate:nil];
    }
    
    + (instancetype)componentWithCollectionView:(UICollectionView *)collectionView type:(LLComponentType)type delegate:(nullable id<LLCollectionComponentDelegate>)delegate {
        Class componentClass = nil;
        switch (type) {
            //根据type来返回相应的子类
            ···
            default:
                break;
        }
        if (componentClass) {
            return [[componentClass alloc] initWithCollectionView:collectionView data:nil delegate:delegate];
        }
        return nil;
    }
    
    #pragma mark - instance method
    - (instancetype)initWithCollectionView:(UICollectionView *)collectionView data:(LLDiscoverRecommendData *)data delegate:(id<LLCollectionComponentDelegate>)delegate {
        self = [super init];
        if (self) {
            self.cellIdentifier = [NSString stringWithFormat:@"%@-Cell", NSStringFromClass(self.class)];
            self.headerIdentifier = [NSString stringWithFormat:@"%@-Header", NSStringFromClass(self.class)];
            self.collectionView = collectionView;
            self.delegate = delegate;
            self.data = data;
            [self registerWithCollectionView:collectionView];
        }
        return self;
    }
    
    #pragma mark - register
    - (void)registerWithCollectionView:(nonnull __kindof UICollectionView *)collectionView {
        [collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:self.cellIdentifier];
        [collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:self.headerIdentifier];
    }
    
    #pragma mark - dataSource
    - (NSUInteger)numberOfItems {
        return 0;
    }
    
    - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
        return CGSizeZero;
    }
    
    - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
        return nil;
    }
    
    - (CGSize)sizeForComponentItemAtIndex:(NSUInteger)index {
        return CGSizeZero;
    }
    
    - (nonnull __kindof UICollectionViewCell *)collectionView:(nonnull UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {
        UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:self.cellIdentifier forIndexPath:indexPath];
        return cell;
    }
    
    #pragma mark - margins
    - (CGFloat)horizontalMargin {
        return 5.0f;
    }
    
    - (UIEdgeInsets)inset {
        return UIEdgeInsetsMake(5, self.horizontalMargin, 5, self.horizontalMargin);
    }
    
    @end
    

    我在基础组件中扩展了例如inset边距,minimumLineSpacing等属性,因为我的项目对这些有要求,同时可以看到我使用了iOS开发中的类族模式,基类通过data或是type来实例化对应的component子类,这样在VC中不用关心对数据的处理细节,并且不用引入大量的子类头文件,真正做到在增删cell时不修改VC代码。

    具体使用

    正如大佬所说,不应该让自己的viewController直接继承自UICollectionViewController,而是继承自UIViewController然后维护一个UICollectionView,因为你的CollectionView并不一定要占满屏幕,它将来可能嵌入其他地方,其他的诸如ScrollView、TableView等都是类似的。

    在UICollectionView的各个代理方法中,将消息转发给对应的component:

    #pragma mark - <UICollectionViewDataSource>
    
    - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
        return self.components.count;
    }
    
    - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
        return [self.components[section] numberOfItems];
    }
    
    - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
        return [self.components[indexPath.section] collectionView:collectionView cellForItemAtIndexPath:indexPath];
    }
    
    - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
        return [self.components[indexPath.section] collectionView:collectionView viewForSupplementaryElementOfKind:kind atIndexPath:indexPath];
    }
    
    #pragma mark - <UICollectionViewDelegateFlowLayout>
    
    - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
        return [self.components[section] minimumLineSpacing];
    }
    //
    - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
        return [self.components[section] minimumInteritemSpacing];
    }
    
    - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
        return [self.components[section] collectionView:collectionView layout:collectionViewLayout referenceSizeForHeaderInSection:section];
    }
    
    - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
        return [self.components[indexPath.section] sizeForComponentItemAtIndex:indexPath.row];
    }
    
    - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
        return self.components[section].inset;
    }
    

    继承基础组件,实现一个子类组件,例如一个普通的一排3个的Collection View Cell:

    @interface LLHomeRcmdBaseComponent()
    
    @property (nonatomic, copy) NSArray<LLDiscoverRcmdOldBlockItem *> *models;
    
    @end
    @implementation LLHomeRcmdBaseComponent
    
    #pragma mark - register
    - (void)registerWithCollectionView:(__kindof UICollectionView *)collectionView {
        [super registerWithCollectionView:collectionView];
        [self.collectionView registerClass:[LLHomeRecommendBaseCell class] forCellWithReuseIdentifier:self.cellIdentifier];
        [self.collectionView registerClass:[LLRecommendBlockSupplementaryView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:self.headerIdentifier];
    }
    
    #pragma mark - dataSource
    - (void)setData:(LLDiscoverRecommendData *)data {
        [super setData:data];
        self.models = data.oldBlock.result;
    }
    
    - (NSUInteger)numberOfItems {
        return self.models.count;
    }
    
    - (CGSize)sizeForComponentItemAtIndex:(NSUInteger)index {
        CGFloat superViewWidth = self.collectionView.superview.frame.size.width;
        CGFloat itemWidth = (superViewWidth - 2 * self.horizontalMargin - 2 * self.minimumInteritemSpacing) / 3;
        return CGSizeMake(itemWidth, 190);
    }
    
    - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
        LLHomeRecommendBaseCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:self.cellIdentifier forIndexPath:indexPath];
        [cell configCellWithModel:self.models[indexPath.row]];
        return cell;
    }
    
    - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
        CGFloat superViewWidth = self.collectionView.superview.frame.size.width;
        return CGSizeMake(superViewWidth, 30);
    }
    
    - (__kindof UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
        if (kind == UICollectionElementKindSectionHeader) {
            LLRecommendBlockSupplementaryView *header = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:self.headerIdentifier forIndexPath:indexPath];
            [header configViewWithTitle:self.data.title];
            return header;
        }
        return nil;
    }
    
    - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndex:(NSUInteger)index {
        
    }
    
    #pragma mark - margins
    
    - (CGFloat)minimumLineSpacing {
        return 10.0f;
    }
    
    - (CGFloat)minimumInteritemSpacing {
        return 5.0f;
    }
    
    @end
    

    在VC中使用时只需要实例化一个component,将其加入VC的components就好了。当数据源不能动态变化时,例如本地mock数据等,如果要再新增一个cell,只需实现对应的cell布局及component组件,将其加入VC的component是就可以了。而当数据源是服务器返回时,只要处理得当,VC中甚至不需任何更改。例如我的写法:

    LLHomeRecommendRequest *request = [LLHomeRecommendRequest sharedInstance];
        __weak typeof(self) weakSelf = self;
        [request loadDataSuccess:^(LLHomeRecommendResponse * _Nullable response) {
            __strong typeof(weakSelf) strongSelf = weakSelf;
            [strongSelf.collectionView.mj_header endRefreshing];
            for (LLDiscoverRecommendData *data in response.oldDataGroup.dataList) {
                LLBaseComponent *component = [LLBaseComponent componentWithCollectionView:strongSelf.collectionView data:data];
                if (component) {
                    [strongSelf.components addObject:component];
                }
            }
            [strongSelf.collectionView reloadData];
        } failure:^(NSString * _Nullable errorMessage) {
            __strong typeof(weakSelf) strongSelf = weakSelf;
            [strongSelf.collectionView.mj_header endRefreshing];
        }];
    

    这样当服务器数据源发生变化,只需新增cell、component、model文件,并在基类中根据数据返回相应的component即可,VC不需任何改动。

    此处的数据请求方法是我对AFNetworking的一个简单封装,目的是使VC无需配置请求参数,解析返回数据。可查阅我的另一篇文章:对AFNetworking的一个简单封装

    思考总结

    我们可能习惯了原生的代理方式,也惯于用switch去判断各种情况,或许觉得这样的实现有点麻烦,不想打破习惯。但是我个人认为,这样的一次技术重构,是会为将来的使用带来极大便利的,代码更易维护,复用性也大大提高,下次写另一个项目时甚至可以直接将这个实现拷贝过去,或者写成pod库供大家使用,这都将对开发效率带来质的提升。

    平时多思考,不要盲目动手。

    相关文章

      网友评论

          本文标题:iOS 一种组件化的Collection View实现

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