iOS 一种组件化的Collection View实现
参考文章: 一种组件化的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等等任何出现在 UICollectionViewDataSource
、UICollectionViewDelegate
、UICollectionViewDelegateFlowLayout
中的代理方法。
//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
库供大家使用,这都将对开发效率带来质的提升。
平时多思考,不要盲目动手。