目标:写可维护的代码。

把 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 层。

1.1 参考项目看这里

把 Data Source 和 Delegate 都分离出来

1.1中的方法是在更轻量的ViewControllers一文中提到的。但该文讲的只是把UITableViewDataSource中的方法提取到一个单独的类,从结果来看,是把numberOfRowsInSectioncellForRowAtIndexPath从 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 的处理,UITableViewDelegateUITableViewDataSource 的方法都已经移到TableViewDataSourceDelegate这个类中处理,但根据
1.2参考项目可以发现1.2的处理方式有局限性,我们观察TableViewDataSourceDelegate中的核心方法:

- (id)initWithItems:(NSArray *)anItems
     cellIdentifier:(NSString *)aCellIdentifier
 configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock
    cellHeightBlock:(CellHeightBlock)aHeightBlock
     didSelectBlock:(DidSelectCellBlock)didselectBlock;

TableViewDataSourceDelegate进行初始化时需传递参数CellIdentifieraHeightBlock,实际开发工作中这两者通常是和UITableViewCell类紧密相关的。一类 UITableViewCell往往对应一个它自己的 CellIdentifiercellHeight,如果初始化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 中。

参考:更轻量的ViewControllers