为 ViewControllers 瘦身
把 TableView 从 VC 中抽离
UITableView 可以说是 iOS 界面开发中用的最广泛的组件,就我自己做过的项目而言,绝大部份 ViewController 都是在围绕 UITableViewDelegate 和 UITableViewDataSource 中的方法打交道。但说到底这些函数都是 View 层面的,全部在 VC 中处理显得不太合适,也不利于日后的维护和扩展,所以关于瘦身 VC 很容易想到一点是从 VC 中抽离 tableView 的表示逻辑。这种思想其实与最近火热的MVVM设计模式相通。核心是把逻辑代码尽量移到 model 层, 你可以认为它是一个中间层 , 逻辑代码可以是各种 delegate,网络请求,缓存,数据库,coredata 等, 而 controller 正是用来组织串联他们,使得整个程序走通。
把 Data Source 分离出来
举例:当需要将一个数组映射到一个 tableView 进行显示,这种一一对应关系可以单独写一个类ArrayDataSource
,使用 block 或者 delegate 设置 cell。ArrayDataSource 类完全可以复用到任何需要将一个数组的内容映射到一个 tableView 的场景。
ArrayDataSource 中声明 block(cell,item)
来初始化 cell,block实现方式(item 和 cell 如何对应)则可以在 cell+Configure 的 category 中声明。
使用ArrayDataSource
,在ViewController
中执行setUpTableView
即可。setUpTableView
中实现 block(可以是执行 configure 方法的方式)。使用 cell 类 category 的方式是为了避免向 dataSource 暴露 cell 的设计,说白了是为了更好得分离 view 和 model 层。
把 Data Source 和 Delegate 都分离出来
1.1中的方法是在更轻量的ViewControllers一文中提到的。但该文讲的只是把UITableViewDataSource
中的方法提取到一个单独的类,从结果来看,是把numberOfRowsInSection
和cellForRowAtIndexPath
从 VC 中提取出去。但实际上UITableViewDelegate
也是可以抽象出去的。例如 cell 的生成, cell 行高, 点击事件等等。这里用 block 实现回调。
处理类TableViewDataSourceDelegate.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef void (^TableViewCellConfigureBlock)(NSIndexPath *indexPath, id item, XTRootCustomCell *cell) ;
typedef CGFloat (^CellHeightBlock)(NSIndexPath *indexPath, id item) ;
typedef void (^DidSelectCellBlock)(NSIndexPath *indexPath, id item) ;
@interface TableViewDataSourceDelegate : NSObject <UITableViewDelegate,UITableViewDataSource>
//初始化方法: 传数据源, cellIdentifier, 三个block分别对应配置, 行高, 点击。
- (id)initWithItems:(NSArray *)anItems
cellIdentifier:(NSString *)aCellIdentifier
configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock
cellHeightBlock:(CellHeightBlock)aHeightBlock
didSelectBlock:(DidSelectCellBlock)didselectBlock ;
//将UITableViewDataSource和UITableViewDelegate设于TableViewDataSourceDelegate。
- (void)handleTableViewDatasourceAndDelegate:(UITableView *)table ;
//默认indexPath.row对应每个dataSource相应返回item。
- (id)itemAtIndexPath:(NSIndexPath *)indexPath ;
@end
在 1.1 中为了避免 model 和 view 的耦合,将 cell 的配置用 category 方法处理。但使用的方式是每一个UITableViewCell
都做了扩展,实际上可以做得更彻底 ——直接对这些子类的父类UITableViewCell
进行扩展。这样做的好处是比 1.1 的扩展方法更加灵活,可以提供多个 configure 方法,针对不同类型的 model 进行数据展示,同时也增强 cell 的移植性。
为 UITableViewCell 提供扩展UITableViewCell+Extension.h
#import <UIKit/UIKit.h>
@interface UITableViewCell (Extension)
//实际是tableView reigister nib,由于是与cell(view)紧密相关的方法,故在cell本身进行实现,而非在vc中。
+(void) registerTable:(UITableView*)table
nibIdentifier:(NSString*)identifier;
//根据数据源配置并绘制cell 子类需重写该方法
-(void)configure:(UITableViewCell*)cell
customObj:(id)obj
indexPath:(NSIndexPath*)indexPath;
//根据数据源计算cell的高度 子类可重写该方法, 若不写为默认值44.0
+(CGFloat)getCellHeightWithCustomObj:(id)obj
indexPath:(NSIndexPath*)indexPath;
@end
如此,对于每一个 cell 子类,实现两个新方法:
- (void)configure:(UITableViewCell *)cell
customObj:(id)obj
indexPath:(NSIndexPath *)indexPath
{
//Rewrite according to your requirements.
}
+ (CGFloat)getCellHeightWithCustomObj:(id)obj
indexPath:(NSIndexPath *)indexPath
{
//Rewrite according to your requirements.
}
OK,做了这么多工作,返回 VC 看一下最后的成果吧!瘦身后的 viewController 对于 tableView 的所有处理都只需要一个方法setUpTableView
- (void)setupTableView
{
TableViewCellConfigureBlock configureCell = ^(NSIndexPath *indexPath, MyObj *obj, XTRootCustomCell *cell) {
[cell configure:cell customObj:obj indexPath:indexPath] ;
} ;
CellHeightBlock heightBlock = ^CGFloat(NSIndexPath *indexPath, id item) {
return [MyCell getCellHeightWithCustomObj:item indexPath:indexPath] ;
} ;
DidSelectCellBlock selectedBlock = ^(NSIndexPath *indexPath, id item) {
NSLog(@"click row : %@",@(indexPath.row)) ;
} ;
self.tableHandler = [[TableDataDelegate alloc] initWithItems:self.list
cellIdentifier:MyCellIdentifier
configureCellBlock:configureCell
cellHeightBlock:heightBlock
didSelectBlock:selectedBlock] ;
[self.tableHander handleTableViewDatasourceAndDelegate:self.table] ;
}
1.2 参考项目看这里
抽离 TableView 终极版
经过 1.2 的处理,UITableViewDelegate
和 UITableViewDataSource
的方法都已经移到TableViewDataSourceDelegate
这个类中处理,但根据
1.2参考项目可以发现1.2的处理方式有局限性,我们观察TableViewDataSourceDelegate
中的核心方法:
- (id)initWithItems:(NSArray *)anItems
cellIdentifier:(NSString *)aCellIdentifier
configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock
cellHeightBlock:(CellHeightBlock)aHeightBlock
didSelectBlock:(DidSelectCellBlock)didselectBlock;
TableViewDataSourceDelegate
进行初始化时需传递参数CellIdentifier
和aHeightBlock
,实际开发工作中这两者通常是和UITableViewCell
类紧密相关的。一类 UITableViewCell
往往对应一个它自己的 CellIdentifier
和 cellHeight
,如果初始化TableViewDataSourceDelegate
对象时就指定这两个属性,则TableViewDataSourceDelegate
仅局限于被一种 tableView 复用:cell种类都相同,也就是用一个tableView展示一个数组( indexPath.row 对应数组下标)。如图:
这些 cell 都是同一种类,但实际开发中往往面临着更复杂的 cell 样式,如我在开发校友圈时:
这种情况怎么使用TableViewDataSourceDelegate
呢?修改TableViewDataSourceDelegate
的 init 方法:
(id)initWithItems:(NSDictionary *)anItems
configureCellBlock:(TableViewCellConfigureCellBlock)aConfigureCellBlock
cellHeightBlock:(CellHeightBlock)aHeightBlock
didSelectBlock:(DidSelectCellBlock)aDidSelectBlock;
业务逻辑移到 model 中
尽管 viewController 最主要功能是处理业务逻辑,但对于一些和 model 联系紧密,和 view 关系不大(即不是 model 和 view 进行交互的逻辑)的代码应移到 model 中,通常是用 category 的处理方法,更加清晰。
网络请求逻辑移到 model
用 category 方式处理。viewController 使用 block 回调请求网络。
view 代码移到 view 层
不要在 viewController 中构建复杂的 view 层次结构。
要注意的是,IB 并非只能和 viewControllers 一起使用,可以加载单独的 nib 文件到自定义的 view 中。