本人录制技术视频地址:https://edu.csdn.net/lecturer/1899 欢迎观看。
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
@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就有这样的例子:
这个时候,我们可以考虑使用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;
};