View controllers 通常是 iOS 项目中最大的文件,并且它们包含了许多不必要的代码。所以 View controllers 中的代码几乎总是复用率最低的。比如UITableView常规用法如下:
传统使用方法
1. 定义数据模型
@interface LFPhoto : NSObject @property (nonatomic,copy) NSString *name; @property (nonatomic,copy) NSString *title; @end
2. 设计自定义LFPhotoCell
#import "LFPhoto.h" @interface LFPhotoCell : UITableViewCell @property (weak, nonatomic) IBOutlet UILabel *name; @property (weak, nonatomic) IBOutlet UILabel *title; @property (nonatomic,strong) LFPhoto *photo; + (instancetype) photoCell; @end @implementation LFPhotoCell + (instancetype) photoCell { return [[[NSBundle mainBundle] loadNibNamed:@"LFPhotoCell" owner:nil options:nil] lastObject]; } - (void)setPhoto:(LFPhoto *)photo { self.name.text = photo.name; self.title.text = photo.title; } @end
3. 利用UITableView展示数据
@interface ViewController () @property (nonatomic,strong) NSArray *photos; @end static NSString *const identifer = @"LFPhotoCell"; @implementation ViewController #pragma mark - Lazy Load - (NSArray *)photos { if (!_photos) { LFPhoto *p1 = [[LFPhoto alloc] init]; p1.name = @"Jason"; p1.title = @"OK"; LFPhoto *p2 = [[LFPhoto alloc] init]; p2.name = @"Rose"; p2.title = @"NO"; LFPhoto *p3 = [[LFPhoto alloc] init]; p3.name = @"Lucy"; p3.title = @"Luck"; _photos = @[p1,p2,p3]; } return _photos; } - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - Data Source - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.photos.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { LFPhotoCell *cell = (LFPhotoCell *)[tableView dequeueReusableCellWithIdentifier:identifer]; if(!cell) { cell = [LFPhotoCell photoCell]; } cell.photo = self.photos[indexPath.row]; return cell; } #pragma mark - Data Delegate - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 80; }
在书写了N次UITableView的代理方法后,我们发现Data Source 方法几乎写法不变,改变的只是Cell和model的类型。所以我们可以考虑把 UITableViewDataSource
的代码提取出来放到一个单独的类中,为View
Controller“瘦身”。
抽取UITableView的DataSource代理方法
要抽取数据源方法,我们定义一个遵守UITableViewDataSource协议的类CommonDataSource。这个类初始化的时候,接受模型数据集合及Cell重用的identifer。最终会将模型数据反馈到Cell上面,所以我们可以再定义一个block,用于回调操作。
// 处理cell与model之间的关系 typedef void (^cellConfig)(id cell, id model); // 实现UITableViewDataSource协议 @interface CommonDataSource : NSObject <UITableViewDataSource> // 自定义数据源 - (instancetype) initWithItem:(NSArray *)items cellIdentifer:(NSString *)identifer cellConfigBlock:(cellConfig)cellConfigBlock; @end @interface CommonDataSource() /*数据集合*/ @property (nonatomic,strong) NSArray *items; @property (nonatomic,copy) NSString *identifer; @property (nonatomic,copy) cellConfig cellBlock; @end @implementation CommonDataSource - (instancetype) initWithItem:(NSArray *)items cellIdentifer:(NSString *)identifer cellConfigBlock:(cellConfig)cellConfigBlock { if(self = [super init]){ self.items = items; self.identifer = identifer; self.cellBlock = [cellConfigBlock copy]; } return self; } #pragma mark - 自定义的类中实现 UITableViewDataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.items.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:self.identifer]; // 这里没有判断cell是否为空,因为在 storyboard中设置了 “prototype cells” 为1,并且将对应的 cell的类型设置为LFPhotoCell了。所以肯定有值。然后在cell上面自定义自己想要的控件,与LFPhotoCell类进行连线。 id item = self.items[indexPath.row]; self.cellBlock(cell,item); return cell; }
代码中dequeueReusableCellWithIdentifer取得的cell一定不为空,因为我在Storyboard中进行了相关属性的设置。
此时,我们就可以在View Controller直接使用自定义的CommonDataSource了,而UITableView的数据源代理方法完全可以取消掉了。
- (void)viewDidLoad { [super viewDidLoad]; // 数据源方法可以进行回调工作 void (^cellBlock)(LFPhotoCell *, LFPhoto *) = ^(LFPhotoCell *cell, LFPhoto *photo){ cell.photo = photo; }; // 准备数据源相关数据 self.myDataSource = [[CommonDataSource alloc] initWithItem:self.photos cellIdentifer:identifer cellConfigBlock:cellBlock]; // 设置数据源 // 当自定义的Data Source 赋值给tableView 的 Data Source的时候,就会调用CommonDataSource 中定义的实现 UITableViewDataSource 的数据源方法 self.tableView.dataSource = self.myDataSource; }
以后使用的时候,我们只需要更改Cell和model的类型就可以了。
同一份View能接受不同模型数据
在实际开发中,有可能有这样的需求:封装了一个View,用来实现某些功能,但有可能数据源有可能不同,美团App就有这样的例子:
“类别分类”与“区域分类”是使用的同一个封装好的View,但他们的数据源明显来自不同的地方。这时候我们该如何处理呢?
这个时候,我们可以考虑使用protocol。以上面的LFPhotoCell为例:
1. 我们定义一个LFPhotoCellItem协议,并且定义指定的实现方法。
/*一个Cell模型,能接受多种模型数据,那么那些模型数据应该遵守cell定义的协议:LFPhotoCellItem,并且实现的模型中要实现协议中定义的方式*/ @protocol LFPhotoCellItem <NSObject> - (NSString *)name; - (NSString *)title; @end
2. 在LFPhotoCell.h中定义协议属性:
@property (assign, nonatomic) id<LFPhotoCellItem> item;
3. 在LFPhotoCell.m中重写协议属性:
- (void)setItem:(id<LFPhotoCellItem>)item { _item = item; self.name.text = [item name]; self.title.text = [item title]; }
4. 每个想传递数据到LFPhotoCell中的模型,必须遵循LFPhotoCellItem协议。
#import "LFPhotoCell.h" @interface LFPhoto : NSObject <LFPhotoCellItem> @property (nonatomic,copy) NSString *name; @property (nonatomic,copy) NSString *title; @end
5. 实现遵守协议的方法
@implementation LFPhoto // 实现LFPhotoCellItem方法 - (NSString *)name { return _name; } - (NSString *)title { return _title; } @end
至此,就完成了协议的自定义工作,现在如果你想更换数据模型的话,只需要将4,5两步的模型更换掉,并且实现协议中定义的方法即可。
最后,在Cell回调中,只需要将传递的具体photo模型更换为item就好了。
void (^cellBlock)(LFPhotoCell *, LFPhoto *) = ^(LFPhotoCell *cell, LFPhoto *photo){ cell.item = photo; };