iOS 一种组件化的Collection View实现

2019-01-07  本文已影响0人  Leemmin

参考文章: 一种组件化的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库供大家使用,这都将对开发效率带来质的提升。

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

上一篇下一篇

猜你喜欢

热点阅读