背景
最近在做一个项目时,需要实现一些列表界面,总体上是上下滚动的,中间的部分段有可以横滚的,有一个个小标签式的,也有可循环滚动的焦点图的……且类似的界面大量出现,并随机组合。可以参照网易云音乐,早期版本的蘑菇街,小红书等等。
按以往的想法是,继承 UITableViewController
然后分多个 section
,所有的数据与点击都在一个 VC 中完成。如果全是占满行的 Cell,勉强可以接受,但很快你会发现,你的代码变得庞大而臃肿,且不可维护。更要命的是,代码无法复用,且一旦有需求变动,留下 Bug 的几率很大。网上关于 UITableView
瘦身的优化方法已经很多了,基本上也就是增加一个 ViewModel 层,将代码换了个地方,没什么很大的意思。但是在这次遇到的项目背景下,我想应该可以用更好的组件化的方式来实现。
组件定义
我们定义 UITableView
中的一个 section
为一个组件(component),它需要管理自己的标头(header)、行高、Cell 数量等:
@protocol RTTableComponent <NSObject>
@required
- (NSString *)cellIdentifier;
- (NSString *)headerIdentifier;
- (NSInteger)numberOfItems;
- (CGFloat)heightForComponentHeader;
- (CGFloat)heightForComponentItemAtIndex:(NSUInteger)index;
- (__kindof UITableViewCell *)cellForTableView:(UITableView *)tableView
atIndexPath:(NSIndexPath *)indexPath;
- (__kindof UIView *)headerForTableView:(UITableView *)tableView;
- (void)reloadDataWithTableView:(UITableView *)tableView
inSection:(NSInteger)section;
- (void)registerWithTableView:(UITableView *)tableView;
@optional
- (void)willDisplayHeader:(__kindof UIView *)header;
- (void)willDisplayCell:(__kindof UITableViewCell *)cell forIndexPath:(NSIndexPath *)indexPath;
- (void)didSelectItemAtIndex:(NSUInteger)index;
@end
上面代码中:- (void)registerWithTableView:(UITableView *)tableView
提供了一个入口供组件注册自定义的 UITableViewCell
。
继承自 UIViewController
——这里不用 UITableViewController
是为了灵活性,比如有时候 TableView
不需要占满屏——实现一个 RTComponentController
,它维护一个成员为 id<RTTableComponent>
类型的数组:
@interface RTComponentController : UIViewController <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, readonly, strong) UITableView *tableView;
@property (nonatomic, strong) NSArray <id<RTTableComponent> > *components;
- (CGRect)tableViewRectForBounds:(CGRect)bounds;
@end
然后在具体的实现中,将大部分 Datasource
和 Delegate
的方法转发到 components
上:
- (CGRect)tableViewRectForBounds:(CGRect)bounds
{
return bounds;
}
#pragma mark - UITableView Datasource & Delegate
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return self.components.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.components[section].numberOfItems;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return self.components[section].heightForComponentHeader;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [self.components[indexPath.section] heightForComponentItemAtIndex:indexPath.row];
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
return [self.components[section] headerForTableView:tableView];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [self.components[indexPath.section] cellForTableView:tableView atIndexPath:indexPath];
}
- (void)tableView:(UITableView *)tableView
willDisplayHeaderView:(UIView *)view
forSection:(NSInteger)section
{
if ([self.components[section] respondsToSelector:@selector(willDisplayHeader:)]) {
[self.components[section] willDisplayHeader:view];
}
}
- (void)tableView:(UITableView *)tableView
willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([self.components[indexPath.section] respondsToSelector:@selector(willDisplayCell:forIndexPath:)]) {
[self.components[indexPath.section] willDisplayCell:cell
forIndexPath:indexPath];
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([self.components[indexPath.section] respondsToSelector:@selector(didSelectItemAtIndex:)]) {
[self.components[indexPath.section] didSelectItemAtIndex:indexPath.row];
}
}
给定一个基础实现 RTBaseComponent
,没有标头,0 个 Cell:
@interface RTBaseComponent : NSObject <RTTableComponent>
@property (nonatomic, weak) id<RTTableComponentDelegate> delegate;
@property (nonatomic, strong) NSString *cellIdentifier;
@property (nonatomic, strong) NSString *headerIdentifier;
+ (instancetype)componentWithTableView:(UITableView *)tableView;
+ (instancetype)componentWithTableView:(UITableView *)tableView delegate:(id<RTTableComponentDelegate>)delegate;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithTableView:(UITableView *)tableView;
- (instancetype)initWithTableView:(UITableView *)tableView delegate:(id<RTTableComponentDelegate>)delegate NS_DESIGNATED_INITIALIZER;
- (void)registerWithTableView:(UITableView *)tableView NS_REQUIRES_SUPER;
- (void)setNeedUpdateHeightForSection:(NSInteger)section;
@end
@interface RTBaseComponent ()
@property (nonatomic, weak) UITableView *tableView;
@end
@implementation RTBaseComponent
- (instancetype)initWithTableView:(UITableView *)tableView delegate:(id<RTTableComponentDelegate>)delegate
{
self = [super init];
if (self) {
self.cellIdentifier = [NSString stringWithFormat:@"%@-Cell", NSStringFromClass(self.class)];
self.headerIdentifier = [NSString stringWithFormat:@"%@-Header", NSStringFromClass(self.class)];
self.tableView = tableView;
self.delegate = delegate;
[self registerWithTableView:tableView];
}
return self;
}
- (void)registerWithTableView:(UITableView *)tableView
{
[tableView registerClass:[UITableViewCell class]
forCellReuseIdentifier:self.cellIdentifier];
}
- (NSInteger)numberOfItems
{
return 0;
}
- (CGFloat)heightForComponentHeader
{
return 0.f;
}
- (CGFloat)heightForComponentItemAtIndex:(NSUInteger)index
{
return 0.f;
}
......
@end
然后继承自 RTBaseComponent
,实现一个有标头的组件:
@interface RTHeaderComponent : RTBaseComponent
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) UIFont *titleFont;
@property (nonatomic, strong) UIColor *titleColor;
@property (nonatomic, strong) UIView *accessoryView;
- (CGRect)accessoryRectForBounds:(CGRect)bounds;
@end
@implementation RTHeaderComponent
- (void)registerWithTableView:(UITableView *)tableView
{
[super registerWithTableView:tableView];
[tableView registerClass:[UITableViewHeaderFooterView class]
forHeaderFooterViewReuseIdentifier:self.headerIdentifier];
}
- (CGFloat)heightForComponentHeader
{
return 36.f;
}
- (__kindof UIView *)headerForTableView:(UITableView *)tableView
{
UITableViewHeaderFooterView *header = [tableView dequeueReusableHeaderFooterViewWithIdentifier:self.headerIdentifier];
header.textLabel.text = self.title;
header.textLabel.textColor = self.titleColor ?: [UIColor darkGrayColor];
self.accessoryView.frame = [self accessoryRectForBounds:header.bounds];
[header.contentView addSubview:self.accessoryView];
return header;
}
- (void)willDisplayHeader:(__kindof UIView *)header
{
UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *)header;
headerView.textLabel.font = self.titleFont ?: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
self.accessoryView.frame = [self accessoryRectForBounds:header.bounds];
}
......
注意,上面需要在 willDisplayHeader:
中设置 textLabel
的字体(可能是苹果的 bug)
同时为了满足横滚等需求,实现一个 RTCollectionComponent
,它管理一个 UICollectionView
实例,实现它的 Datasource 和 Delegate,提供一个入口供子类注册自定义的 UICollectionViewCell
,并最终将它添加到 cell.contentView
上:
@interface RTCollectionComponent : RTActionHeaderComponent <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout>
@property (nonatomic, readonly, strong) UICollectionView *collectionView;
- (void)configureCollectionView:(UICollectionView *)collectionView NS_REQUIRES_SUPER;
- (CGRect)collectionViewRectForBounds:(CGRect)bounds;
@end
结果
在 Demo 中项目自定义了四种 Component:
RTDemoTagsComponent
RTDemoBannerComponent
RTDemoImageItemComponent
RTDemoItemComponent
最终实现的界面效果类似如下:
19-5.png 19-6.png
而整个 VC 的代码只是挂载了四个 Component,在其他 VC 中这些组件也可以选择性地复用,且有较高的配置灵活性:
- (void)viewDidLoad {
[super viewDidLoad];
RTDemoTagsComponent *tags = [RTDemoTagsComponent componentWithTableView:self.tableView
delegate:self];
self.components = @[tags,
[RTDemoImageItemComponent componentWithTableView:self.tableView
delegate:self],
[RTDemoBannerComponent componentWithTableView:self.tableView
delegate:self],
[RTDemoImageItemComponent componentWithTableView:self.tableView
delegate:self],
[RTDemoItemComponent componentWithTableView:self.tableView
delegate:self]];
[tags reloadDataWithTableView:self.tableView
inSection:0];
}
单个 Component 的数据可以由 VC 发起请求后一起塞回,或者每个 Component 自己在 - (void)reloadDataWithTableView:inSection:
方法中请求,而 VC 负责触发一次请求,取决于具体实现与需求。
总结
一个程序员的日常无非就是在处理产品经理的各种合理非理的需求,在真正动手之前多停下来思考一下,磨刀不误砍柴功,以不变应对万变的需求。在上面这种实现中,无论临时增加或减少一个展示段,无非就是增加、减少一个 Component,修改起来没有痛苦。而如果像以前一样用 switch (indexPath.section)
的办法,不仅改起来不方便,还容易 Crash。
以上所有代码匀可以在 Github 上找到,并已经发布到 Cocoapods
本文只针对
UITableView
做了简单的组件化,同样的操作可以应用到UICollectionView
上,且更多实用,并且现在已经有开源实现:https://github.com/Instagram/IGListKit,或者 DDComponent,使用更简单。如何更全面的、完全的组件化?参考以下两个实现:HubFramework,ComponentKit
网友评论