基于RAC的通用TableView
最近公司的一个新项目的1.0版本开发完了,但是对于这么一个初期的项目,部分VC的代码行数仍然超过300行。我也开始感觉到有种(Massive)MVC的趋势,而且部分界面控件的创建方法还是略略有点Magic。于是我开始寻求新的架构,来改善当前的状况。我想起了之前听说过的ReactiveCocoa,加上最近Ray神和Objcio.cn的介绍,我也开始了RAC的“修炼”。
不过,RAC的学习不但需要了解其API的作用,更重要的是用RAC的思维去思考。如果用传统的MVC架构思维,我可以很快速写出一个简单的框架,但是学习RAC和MVVM,就发现原来很多理所当然的东西也要仔细去思考。对于初学者而言,RAC并不是很友好,但掌握基本用法后,就已经可以感受到RAC的优势。
在学习的时候,我看到了Ray神上面用Signal和Command来代替UITableViewDataSource和UITableViewDelegate的helper。不过,本人还是觉得不太满意,毕竟需要额外的helper,而且限定cell必须来自xib。于是基于Ray神的基础上,我把这套模式搬到了UITableView层上,同时也支持多个section和cell的定制。这类相对常用的控件,我不想把API设计得过于复杂,所以对于有相对特殊的要求还是需要实现DataSource和Delegate。本人意在把Ray神的思路整合到控件层,并不打算写高大上的控件。
1 // 2 // PINKBindCellProtocol.h 3 // 4 5 #import <Foundation/Foundation.h> 6 7 @protocol PINKBindCellProtocol <NSObject> 8 9 - (void)bindCellViewModel:(id)viewModel; 10 11 @end
1 // 2 // PINKBindTableView.h 3 // 4 5 #import <UIKit/UIKit.h> 6 7 @protocol PINKBindCellProtocol; 8 9 typedef UITableViewCell *(^PINKBindTableViewCreateCellBlock)(NSIndexPath *indexPath); 10 11 @interface PINKBindTableView : UITableView 12 13 @property (nonatomic, getter = isAutoCheckDataSource) BOOL autoCheckDataSource; 14 @property (nonatomic, getter = isAutoDeselect) BOOL autoDeselect; 15 16 @property (nonatomic, readonly) id<UITableViewDataSource> realDataSource; 17 @property (nonatomic, readonly) id<UITableViewDelegate> realDelegate; 18 19 - (void)setDataSourceSignal:(RACSignal *)sourceSignal 20 selectionCommand:(RACCommand *)selection 21 cellClass:(Class<PINKBindCellProtocol>)cellClass; 22 23 - (void)setDataSourceSignal:(RACSignal *)sourceSignal 24 selectionCommand:(RACCommand *)selection 25 createCellBlock:(PINKBindTableViewCreateCellBlock)createCellBlock; 26 27 @end 28 29 //以下是截获了的方法,继承子类时可以直接重写,但还是要调用super 30 @interface PINKBindTableView (UITableViewDataSourceIntercept) 31 32 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView; 33 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; 34 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; 35 36 @end 37 38 @interface PINKBindTableView (UITableViewDelegateIntercept) 39 40 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath; 41 42 @end
1 // 2 // PINKBindTableView.m 3 // 4 5 #import "PINKBindTableView.h" 6 7 #import "MessageInterceptor.h" 8 9 #import "PINKBindCellProtocol.h" 10 11 typedef NS_OPTIONS(NSInteger, PINKBindTableView_DataSource_MethodType) { 12 PINKBindTableView_DataSource_MethodType_numberOfSectionsInTableView = 1 << 0, 13 PINKBindTableView_DataSource_MethodType_numberOfRowsInSection = 1 << 1, 14 PINKBindTableView_DataSource_MethodType_cellForRowAtIndexPath = 1 << 2, 15 }; 16 17 @interface PINKBindTableView ()<UITableViewDataSource, UITableViewDelegate> 18 { 19 MessageInterceptor *_dataSourceInterceptor; 20 MessageInterceptor *_delegateInterceptor; 21 22 PINKBindTableView_DataSource_MethodType _dataSourceMethodType; 23 } 24 25 /** 26 * tableData为nil时,若dataSource或delegate实现了对应方法,则调用。 27 */ 28 @property (nonatomic, strong) NSArray *tableData; 29 @property (nonatomic, strong) RACCommand *didSelectedCommand; 30 @property (nonatomic, unsafe_unretained) Class<PINKBindCellProtocol> cellClass; 31 @property (nonatomic, strong) NSString *cellReuseIdentifier; 32 @property (nonatomic, copy) PINKBindTableViewCreateCellBlock createCellBlock; 33 34 @end 35 36 @implementation PINKBindTableView 37 38 - (id)initWithFrame:(CGRect)frame 39 { 40 self = [super initWithFrame:frame]; 41 42 if (self) { 43 [self p_InitPINKBindTableView]; 44 } 45 46 return self; 47 } 48 49 - (instancetype)initWithCoder:(NSCoder *)aDecoder 50 { 51 self = [super initWithCoder:aDecoder]; 52 53 if (self) { 54 [self p_InitPINKBindTableView]; 55 } 56 57 return self; 58 } 59 60 #pragma mark - Private Initialize 61 - (void)p_InitPINKBindTableView 62 { 63 _autoCheckDataSource = YES; 64 _autoDeselect = YES; 65 66 _dataSourceInterceptor = [[MessageInterceptor alloc] init]; 67 _dataSourceInterceptor.middleMan = self; 68 _dataSourceInterceptor.receiver = [super dataSource]; 69 [super setDataSource:(id<UITableViewDataSource>)_dataSourceInterceptor]; 70 71 _delegateInterceptor = [[MessageInterceptor alloc] init]; 72 _delegateInterceptor.middleMan = self; 73 _delegateInterceptor.receiver = [super delegate]; 74 [super setDelegate:(id<UITableViewDelegate>)_delegateInterceptor]; 75 } 76 77 #pragma mark - Overwrite DataSource 78 - (void)setDataSource:(id<UITableViewDataSource>)dataSource 79 { 80 if (_dataSourceInterceptor) { 81 _dataSourceInterceptor.receiver = dataSource; 82 //UITableViewDataSource有类似缓存机制优化,所以先设置nil 83 [super setDataSource:nil]; 84 [super setDataSource:(id<UITableViewDataSource>)_dataSourceInterceptor]; 85 86 [self updateDataSourceMethodType]; 87 } else { 88 [super setDataSource:dataSource]; 89 } 90 } 91 92 - (id<UITableViewDataSource>)realDataSource 93 { 94 if (_dataSourceInterceptor) { 95 return _dataSourceInterceptor.receiver; 96 } else { 97 return [super dataSource]; 98 } 99 } 100 101 #pragma mark - Overwrite Delegate 102 - (void)setDelegate:(id<UITableViewDelegate>)delegate 103 { 104 if (_delegateInterceptor) { 105 _delegateInterceptor.receiver = delegate; 106 107 [super setDelegate:nil]; 108 [super setDelegate:(id<UITableViewDelegate>)_delegateInterceptor]; 109 } else { 110 [super setDelegate:delegate]; 111 } 112 } 113 114 - (id<UITableViewDelegate>)realDelegate 115 { 116 if (_delegateInterceptor) { 117 return _delegateInterceptor.receiver; 118 } else { 119 return [super delegate]; 120 } 121 } 122 123 #pragma mark - UITableViewDataSource 124 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 125 { 126 if (!_tableData && 127 _dataSourceMethodType & PINKBindTableView_DataSource_MethodType_numberOfSectionsInTableView) { 128 return [_dataSourceInterceptor.receiver numberOfSectionsInTableView:tableView]; 129 } else 130 return _tableData.count; 131 } 132 133 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 134 { 135 if (!_tableData && 136 _dataSourceMethodType & PINKBindTableView_DataSource_MethodType_numberOfRowsInSection) { 137 return [_dataSourceInterceptor.receiver tableView:tableView numberOfRowsInSection:section]; 138 } else 139 return [_tableData[section] count]; 140 } 141 142 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 143 { 144 if (!_tableData && 145 _dataSourceMethodType & PINKBindTableView_DataSource_MethodType_cellForRowAtIndexPath) { 146 return [_dataSourceInterceptor.receiver tableView:tableView cellForRowAtIndexPath:indexPath]; 147 } else { 148 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:_cellReuseIdentifier]; 149 if (!cell) { 150 if (_cellClass) { 151 cell = [[(Class)_cellClass alloc] init]; 152 } else if (_createCellBlock) { 153 cell = _createCellBlock(indexPath); 154 } 155 } 156 [(id<PINKBindCellProtocol>)cell bindCellViewModel:_tableData[indexPath.section][indexPath.row]]; 157 158 return cell; 159 } 160 } 161 162 #pragma mark - UITableViewDelegate 163 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 164 { 165 if (_autoDeselect) { 166 [tableView deselectRowAtIndexPath:indexPath animated:YES]; 167 } 168 169 if (_tableData && _didSelectedCommand) { 170 [_didSelectedCommand execute:_tableData[indexPath.section][indexPath.row]]; 171 } 172 173 if ([_delegateInterceptor.receiver respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) { 174 [_delegateInterceptor.receiver tableView:tableView didSelectRowAtIndexPath:indexPath]; 175 } 176 } 177 178 #pragma mark - 缓存DataSource方法 179 - (void)updateDataSourceMethodType 180 { 181 _dataSourceMethodType = 0; 182 183 if ([_dataSourceInterceptor.receiver respondsToSelector:@selector(numberOfSectionsInTableView:)]) { 184 _dataSourceMethodType |= PINKBindTableView_DataSource_MethodType_numberOfSectionsInTableView; 185 } 186 187 if ([_dataSourceInterceptor.receiver respondsToSelector:@selector(tableView:numberOfRowsInSection:)]) { 188 _dataSourceMethodType |= PINKBindTableView_DataSource_MethodType_numberOfRowsInSection; 189 } 190 191 if ([_dataSourceInterceptor.receiver respondsToSelector:@selector(tableView:cellForRowAtIndexPath:)]) { 192 _dataSourceMethodType |= PINKBindTableView_DataSource_MethodType_cellForRowAtIndexPath; 193 } 194 } 195 196 #pragma mark - API 197 - (void)setDataSourceSignal:(RACSignal *)sourceSignal 198 selectionCommand:(RACCommand *)selection 199 cellClass:(Class<PINKBindCellProtocol>)cellClass 200 { 201 self.cellClass = cellClass; 202 self.createCellBlock = nil; 203 self.didSelectedCommand = selection; 204 [self configCellReuseIdentifierAndCellHeight]; 205 206 @weakify(self); 207 [sourceSignal subscribeNext:^(NSArray *source) { 208 @strongify(self); 209 self.tableData = source; 210 [self reloadData]; 211 }]; 212 } 213 214 - (void)setDataSourceSignal:(RACSignal *)sourceSignal 215 selectionCommand:(RACCommand *)selection 216 createCellBlock:(PINKBindTableViewCreateCellBlock)createCellBlock 217 { 218 self.cellClass = nil; 219 self.createCellBlock = createCellBlock; 220 self.didSelectedCommand = selection; 221 [self configCellReuseIdentifierAndCellHeight]; 222 223 @weakify(self); 224 [sourceSignal subscribeNext:^(NSArray *source) { 225 @strongify(self); 226 self.tableData = source; 227 [self reloadData]; 228 }]; 229 } 230 231 - (void)configCellReuseIdentifierAndCellHeight 232 { 233 UITableViewCell *oneCell = nil; 234 if (_cellClass) { 235 oneCell = [[(Class)_cellClass alloc] init]; 236 } else if (_createCellBlock) { 237 oneCell = _createCellBlock([NSIndexPath indexPathForRow:0 inSection:0]); 238 } 239 _cellReuseIdentifier = oneCell.reuseIdentifier; 240 self.rowHeight = oneCell.frame.size.height; 241 } 242 243 #pragma mark - Properties 244 - (void)setTableData:(NSArray *)tableData 245 { 246 if (_autoCheckDataSource && 247 tableData.count && 248 ![tableData[0] isKindOfClass:[NSArray class]]) { 249 _tableData = @[tableData]; 250 } else { 251 _tableData = tableData; 252 } 253 } 254 255 @end