刚进公司不到一个月,接到一个需求,把一个项目的界面改一下。看了项目里的视图,耦合性强,没有复用,改起 UI 来很费劲。新的界面是个列表视图,就寻思怎么写出一个后面接手的人改起 UI 不那么费劲的 Tableview,顺便把项目里老的 CollectionView 也进行了类比,偷偷进行了重构。毕竟 UI 总是改来改去的,而常用的 Tableview 和 Collecitonview 更是应该让其扩展性更好,复用性更强。
先从 Tableview 讲起,Tableview 都很熟悉,一般就是由 datasource,delegate,cell,cellitem 这几部分组成。
打造一个通用性更强的 DataSource
新建一个 NSObject
的子类,命名为 CDZTableDataSource,并遵循 UITableViewDataSource
协议。对于 datasource 来说,必须实现的方法是 -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
和 -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
。
先看看第一个方法,返回每个 section 的 row 的数目,这个数字怎么来呢,就是 section 里的数据条数。如果不实现 optional 方法 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
的话,默认只有一个 section。所以我们首先要确定的就是 section 的数目,所以对应的,我们应该有 Section 的 Model。新建一个 NSObject
的子类,命名为 CDZSectionObject。那么这个 Section 应该有些什么属性呢?对于一个 Section 对象来说,应该持有一个 row 对应的 Model 的数组,这样就可以确定每个 section 的 row 的数量了。可能还有一些 sectionHeader 是数据等等。
@property (nonatomic, copy) NSString *headerTitle;
@property (nonatomic, copy) NSString *headerReuseIdentifer;
@property (nonatomic, strong) NSMutableArray *itemsArray;
回到 Datasouce,那么 Datasouce 就可以加上这些属性。
@property (nonatomic, strong) NSMutableArray<CDZSectionObject *> *sectionsArray;
@property (nonatomic, strong) CDZSectionObject *firstSection;
那么现在就可以实现第一个方法了,为了方便我们还可以默认有一个 firstSection,方便只有一个 section 的情况使用。
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
if (self.sectionsArray.count > section) {
return self.sectionsArray[section].itemsArray.count;
}
return 0;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return self.sectionsArray ? self.sectionsArray.count : 0;
}
- (CDZSectionObject *)firstSection{
if (!_firstSection) {
_firstSection = [[CDZSectionObject alloc]init];
[self.sectionsArray addObject:_firstSection];
}
return _firstSection;
}
轮到第二个方法了,配置 cell 的时候,我们需要知道 cell 对应的 item 而去配置 cell,那么更抽象一点的话,也就是说 Datasource 是建立了cellitem 和 cell 之间的联系,也就是 Model-View 直接的联系。View 本身做的工作也就是根据 Model 配置 View 的样子。所以这时候我们我们可以定义一个接口。
@protocol CDZCustomView
@required
- (void)setItem:(id)item;
@end
然后让我们的 Cel l遵守这个协议,也就是所有通用的 Cell,需要在方法内实现 item 的解析。而其实对于 cell 也是一种 view,这个接口可以让一些复用的 view 都遵守,比如 headerView 等等。对于 cell 我们还可以拓展这个协议。
@protocol CDZTableCell <CDZCustomView>
@required
+ (CGFloat)tableView:(UITableView *)tableView rowHeightForItem:(id)item;
@end
这样,cell 的高度也可以由 cell 内部自己决定,适合各种各样的 cell,当然也可以让 Autolayout 决定。但是有一些老项目里的 cell 里面没有 Autolayout,所以返回高度通用性可能更强些。
回到 Datasource,我们解决了 cell 的 Model 问题,下一个是,cell 的 class 怎么决定呢?其实对于 Datasouce 来说,cell 的类型,往往是由对应的 Model 决定的,也就是 Model 的类型,决定可以使用的 cell 的种类。那么我们在 Datasouce 里,只要给出一个接口,让使用者建立这种联系,item class - cell class 的联系,并在内部用一个类似哈希表的东西储存起来,我们便可以根据 item 找到 cell 的 class。简单的话用一个 NSMutableDictionary
就可以了。
- (void)setCellClass:(Class)cellClass forItemClass:(Class)itemClass{
if (!itemClass || !cellClass) {
return;
}
[self.cellDic setObject:cellClass forKey:NSStringFromClass(itemClass)];
}
而这样以后要更换 View 的时候,只要新建一个 cell 并重新 set 一下对应的 item 的就可以了,换 Model 也类似。
那么现在就可以根据 index 找到对应的 Section 和对应的 row 所对应的 item 所对应的 cellclass 了。
- (id)tableView:(UITableView *)tableView itemForRowAtIndexPath:(NSIndexPath *)indexPath{
if (self.sectionsArray.count > indexPath.section) {
if (self.sectionsArray[indexPath.section].itemsArray.count > indexPath.row) {
return self.sectionsArray[indexPath.section].itemsArray[indexPath.row];
}
}
return nil;
}
- (Class)tableView:(UITableView *)tableView cellClassForRowAtIndexPath:(NSIndexPath *)indexPath{
id item = [self tableView:tableView itemForRowAtIndexPath:indexPath];
Class cellClass = [self.cellDic objectForKey:NSStringFromClass([item class])];
if (!cellClass) {
cellClass = [UITableViewCell class];//没有对应关系的,取默认的UITalbeViewCell
}
return cellClass;
}
那么 cellforrow 方法就出来了
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
Class cellClass = [self tableView:tableView cellClassForRowAtIndexPath:indexPath];
UITableViewCell<CDZTableCell> *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(cellClass)];
if (!cell) {
cell = [[cellClass alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(cellClass)];
}
if ([cell respondsToSelector:@selector(setItem:)]) {//检查是否有实现setItem的方法
[cell setItem:[self tableView:tableView itemForRowAtIndexPath:indexPath]];
}
return cell;
}
到此 DataSource 就完成了。
使用方法
让自定义的 cell 遵守 CDZTableCell 协议,内部实现 setItem 方法去配置 View。一些老的 cell 也可以简单的遵守协议并把以前配置视图的方法写到 setItem 方法里就可以了。协议比继承耦合性更低些,即加即用。
让 Tableview 的 Datasouce 指定为一个 CDZTabeDataSource,并设置 item 对应的 cell 类型就可以。更换 cell 和 item 只要重新建立关系即可,达到了 Datasource 的复用和拓展性。对于各种 Tableview,就不用重复写 Datasource 的代码了。例子如 Demo:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
Class cellClass = [self.tableDataSource tableView:tableView cellClassForRowAtIndexPath:indexPath];
return (cellClass == [UITableViewCell class]) ? 44.f : [cellClass tableView:tableView rowHeightForItem:[self.tableDataSource tableView:tableView itemForRowAtIndexPath:indexPath]];
}
- (UITableView *)tableView{
if (!_tableView) {
_tableView = [[UITableView alloc]initWithFrame:self.view.frame style:UITableViewStylePlain];
_tableView.delegate = self;
_tableView.dataSource = self.tableDataSource;
}
return _tableView;
}
- (CDZTableDataSource *)tableDataSource{
if (!_tableDataSource) {
_tableDataSource = [[CDZTableDataSource alloc]init];
[_tableDataSource setCellClass:[CDZTableViewCell class] forItemClass:[CDZCellItem class]];
_tableDataSource.firstSection.itemsArray = [[self mockItems] copy];
}
return _tableDataSource;
}
CollecitonViewDatasource 其实也类似
区别在于 collecitonview 的 cell 要注册,所以 cellForRow 方法有所不同
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
Class cellClass = [self collectionView:collectionView cellClassForRowAtIndexPath:indexPath];
UICollectionViewCell<CDZCollectionCell> *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass(cellClass) forIndexPath:indexPath];
if ([cell respondsToSelector:@selector(setItem:)]) {
[cell setItem:[self collectionView:collectionView itemForRowAtIndexPath:indexPath]];
}
return cell;
}
其它基本类似,且 Collectionview 和 Tableview 还可以共用一个 sectionobject。
最后
所有源码和 Demo 工作里,我们经常会因为赶需求而做一些复制粘贴。但也别忘了多思考如何复用,如何类比,虽然花掉现在一些时间,却会在日后的重构,修改,别人的修改上带来很多便利。
如果您觉得有帮助,不妨给个 star 鼓励一下,欢迎关注&交流