一、UITableView中如何適應需求多變(新增刪除、經常調換位置、高度變動等等)的通用解決方法
拿我負責的樓盤詳情來說:
因為產品會不時的參考運維及競品產品,所以也會不時地對樓盤各個模塊進行迭代調整,如果采用 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row == 0) { //dosomething } else if (indexPath.row == 1) { //dosomething } } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row == 0) { //didSelect } else if (indexPath.row == 1) { //didSelect } }
進行代碼兼容,對應的其他方法也得細心細心是的修正,想想都覺得可怕而又不保險,經過長期的磨合及快速適應產品需求而又讓自己身心愉悅,必須得有一套完整而又通用的模式。
遵循一切皆對象的思維方式,我采取了 不同模塊盡量使用獨立的cell 處理,比如
這一塊,盡量分兩個cell實現,畢竟下一次需求 地址 和 最新開盤 就分開了。
當然一個項目最好能有一個基類的UITableViewCell , 比如這樣的:
@interface FDDBaseTableViewCell(ObjectType)(識別問題,此處圓括號替換尖括號): UITableViewCell @property (nonatomic,weak) id(FDDBaseTableViewCellDelegate)(識別問題,此處圓括號替換尖括號) fddDelegate; @property (nonatomic,strong) ObjectType fddCellData; + (CGFloat)cellHeightWithCellData:(ObjectType)cellData; - (void)setCellData:(ObjectType)fddCellData; @end
再者,隨著 MVVM 模式的普及,項目中我也使用了一個中間的 cellModel 來控制 UITableView 對 UITableViewCell 的構建:
@interface FDDBaseCellModel : NSObject @property (nonatomic, strong) id cellData; //cell的數據源 @property (nonatomic, assign) Class cellClass; //cell的Class @property (nonatomic, weak) id delegate; //cell的代理 @property (nonatomic, assign) CGFloat cellHeight; //cell的高度,提前計算好 @property (nonatomic, strong) FDDBaseTableViewCell *staticCell; //兼容靜態的cell + (instancetype)modelFromCellClass:(Class)cellClass cellHeight:(CGFloat)cellHeight cellData:(id)cellData; - (instancetype)initWithCellClass:(Class)cellClass cellHeight:(CGFloat)cellHeight cellData:(id)cellData; @end
一套通用構建 UITableView 的大致的思路如下:
對應的代碼也就是這樣:
- (void)disposeDataSources{ NSArray *randomSources = @[@"Swift is now open source!", @"We are excited by this new chapter in the story of Swift. After Apple unveiled the Swift programming language, it quickly became one of the fastest growing languages in history. Swift makes it easy to write software that is incredibly fast and safe by design. Now that Swift is open source, you can help make the best general purpose programming language available everywhere", @"For students, learning Swift has been a great introduction to modern programming concepts and best practices. And because it is now open, their Swift skills will be able to be applied to an even broader range of platforms, from mobile devices to the desktop to the cloud.", @"Welcome to the Swift community. Together we are working to build a better programming language for everyone.", @"– The Swift Team"]; for (int i=0; i<30; i++) { NSInteger randomIndex = arc4random() % 5; FDDBaseCellModel *cellModel = [FDDBaseCellModel modelFromCellClass:HDTableViewCell.class cellHeight:[HDTableViewCell cellHeightWithCellData:randomSources[randomIndex]] cellData:randomSources[randomIndex]]; [self.dataArr addObject:cellModel]; } } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return self.dataArr.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ FDDBaseCellModel *cellModel = self.dataArr[indexPath.row]; FDDBaseTableViewCell *cell = [tableView cellForIndexPath:indexPath cellClass:cellModel.cellClass]; [cell setCellData:cellModel.cellData delegate:self]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ FDDBaseCellModel *cellModel = self.dataArr[indexPath.row]; //dosomething }
也就是無論有多少種不同類型、各種順序排列的 UITableViewCell ,我們只需要關注數據源中的FDDBaseCellModel即可 ,而且 UITableViewDataSource 中的協議方法變得極為的簡潔和通用。
二、如何避免同一套完全相同的UITableViewDelegate、UITableViewDataSource代碼在不同UIViewController多次實現
有了前面的構想,我們會驚奇的發現,實現一個無論簡單或者復雜的 UITableView 僅僅取決於包含 FDDBaseCellModel 的數據源!而所有包含 UITableView 的 UIViewController 的 UITableViewDelegate、UITableViewDataSource 代碼完全一致!
那麼問題來了,怎麼避免有如此的的重復代碼在你優秀的項目中呢?
1、繼承幫你忙:
在項目的 UIViewController 基類中,實現通用的 UITableViewDelegate、UITableViewDataSource 方法即可,畢竟數據源 self.dataArr 可以放在基類中,子類如果確實有通用方法無法處理的特殊情況,沒有問題!各自子類重載對應的方法即可。Objective-C 和 Swift 通用。
存在的問題:
對非繼承基類的 UIViewController 無力回天;
對 UIView 中包含的 UITableView 無法做到兼容;
當 UITableViewDelegate、UITableViewDataSource不是交給當前 UIViewController 時;
等等等。。。
2、中間轉換類(FDDTableViewConverter)實現:
2.1、通過響應模式來實現:
只需要判斷 UITableView 的載體是否能響應對應的 UITableViewDelegate、UITableViewDataSource 方法,如果載體實現則使用載體本身的方法即可,這個其實和繼承中重載的思路一致,但是少了一層繼承依賴關系總是好的。Swift 不可用。
存在的問題:
和繼承方式一樣,需要在當前類響應 UITableViewDelegate、UITableViewDataSource 方法;
當 UITableViewDelegate、UITableViewDataSource 不是交給當前 UIViewController 時;
因為載體不在遵循 UITableViewDelegate、UITableViewDataSource,寫對應的方法是編譯器無法給到代碼聯想補全功能,略尴尬。
中間轉換類需要實現大部分的 UITableViewDelegate、UITableViewDataSource 方法,盡量全面寫完;
響應模式中因為要在 轉換類 中調用載體的方法、提供不定向的入參及接收返回值,使用 performSelector: 方法則不可行,在 Objective-C 中倒是可以使用 NSInvocation 實現,但是在 Swift 中 NSInvocation 已經被廢棄,也就是只能兼容 Objective-C 代碼。如果有其他方式兼容 swift 請立馬告知我,謝謝!
等等等。。。
2.2、通過注冊模式來實現:
這種思維模式和AOP切片模式很像,哪裡注冊了 UITableViewDelegate、UITableViewDataSource 方法,哪裡處理改方法,沒有默認的統一走 中間轉換類 的統一處理。實現方式是通過 NSMutableDictionary 來保存注冊的 SEL 和 resultBlock 。resultBlock 傳參放入一個數組中,個數和 SEL 中的入參保持一致,返回值是注冊的載體返回給 中間轉換類 的結果, 中間轉換類 拿到這個值再給到 UITableViewDelegate、UITableViewDataSource 。好像有點轉,看代碼你肯定就清晰了:
FDDTableViewConverter 部分代碼:
typedef id (^resultBlock)(NSArray *results); @interface FDDTableViewConverter(TableViewCarrier): NSObject (UITableViewDataSource, UITableViewDelegate) (識別問題,此處圓括號替換尖括號) //默認模式,使用注冊方式處理tableView的一些協議 @property (nonatomic, assign) FDDTableViewConverterType converterType; // 只有在選擇 FDDTableViewConverter_Register 模式時,才會block回調 - (void)registerTableViewMethod:(SEL)selector handleParams:(resultBlock)block; @end
UITableView 載體 ViewController 部分代碼:
- (void)disposeTableViewConverter{ _tableViewConverter = [[FDDTableViewConverter alloc] initWithTableViewCarrier:self daraSources:self.dataArr]; UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain]; tableView.delegate = _tableViewConverter; tableView.dataSource = _tableViewConverter; tableView.separatorStyle = UITableViewCellSeparatorStyleNone; [self.view addSubview:tableView]; __weak typeof(self) weakSelf = self; [_tableViewConverter registerTableViewMethod:@selector(tableView:cellForRowAtIndexPath:) handleParams:^id(NSArray *results) { UITableView *tableView = results[0]; NSIndexPath *indexPath = results[1]; FDDBaseCellModel *cellModel = weakSelf.dataArr[indexPath.row]; FDDBaseTableViewCell *cell = [tableView cellForIndexPath:indexPath cellClass:cellModel.cellClass]; [cell setCellData:cellModel.cellData delegate:weakSelf]; return cell; }]; }
這種方式暫時看來是比較可取的方式了,無論從代碼的整潔還是耦合度來說都是非常棒的模式了,而且它關注的是誰注冊了對應的方法,你就在block拿到 中間轉換類 的值來實現你特殊化的 UITableView , 再回傳給 中間轉換類 來替你實現。而且注冊的 SEL 有代碼聯想補全功能,Objective-C 和 Swift 通用。
存在的問題:
中間轉換類需要實現大部分的 UITableViewDelegate、UITableViewDataSource 方法,盡量全面寫完。
等等等。。。
3、Swift通過Category實現:
Swift 和 Objective-C 的 Category 實現機制是不一樣的,對於 Objective-C 來說,當前類和 Category 有相同方法時會優先執行 Category 中的方法,但是在 Swift 的世界裡,同時存在同一個方法是不允許的,所以也就多了一個 override 關鍵字來優先使用當前類的方法。
實現方式也就是在 UITableView 載體的 Category 中實現通用的代碼,然後使用 override 關鍵字來特殊化需要特殊處理的 方法即可。
比如這樣:
FDDTableViewConverter.swift
extension FDDBaseViewController: FDDBaseTableViewCellDelegate { @objc(tableView:numberOfRowsInSection:) func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.dataArr.count } @objc(tableView:cellForRowAtIndexPath:) func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cellModel: FDDBaseCellModel = self.dataArr.object(at: indexPath.row) as! FDDBaseCellModel let cell: FDDBaseTableViewCell = tableView.cellForIndexPath(indexPath, cellClass: cellModel.cellClass)! cell.setCellData(cellModel.cellData, delegate: self) cell.setSeperatorAtIndexPath(indexPath, numberOfRowsInSection: self.dataArr.count) return cell } @objc(tableView:heightForRowAtIndexPath:) func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let cellModel: FDDBaseCellModel = self.dataArr.object(at: indexPath.row) as! FDDBaseCellModel return CGFloat(cellModel.cellHeight) } }
ViewController.swift
override internal func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 10 }
這種模式代碼量非常少,並且實現起來很方便,Objective-C 不可用。
存在的問題:
當在引入 FDDTableViewConverter.swift 後,因為Swift項目的特殊性(同模塊中不需要導入該文件即可使用),這會導致以前的代碼中,不是通用代碼能實現的 UITableViewDelegate、UITableViewDataSource 方法前面都得加上 override 關鍵字;
和繼承有同樣的毛病,不同的載體需要寫上對應的category,貌似這塊代碼又是重復代碼,苦逼;
等等等。。。
三、小結:
上面的兩個問題點是同事 @袁強 拋出給到我,但是解決問題的思路很多出至於 @凌代平 ,很慶幸有這麼一次機會來碼磚的機會。相信還會有其他更好的思路,如果你正好看到了請不吝賜教,
代碼整潔的道路很遠,我相信只要需求理解到位,代碼設計合理,我相信以後我們的實現 UITableView 時,只需要如下代碼:
@implementation ViewController - (void)dealloc{ NSLog(@"%@ dealloc", NSStringFromClass(self.class)); } - (void)viewDidLoad { [super viewDidLoad]; self.title = @"ViewController"; [self disposeDataSources]; [self disposeTableViewConverter]; } - (void)disposeDataSources{ NSArray *randomSources = @[@"Swift is now open source!", @"We are excited by this new chapter in the story of Swift. After Apple unveiled the Swift programming language, it quickly became one of the fastest growing languages in history. Swift makes it easy to write software that is incredibly fast and safe by design. Now that Swift is open source, you can help make the best general purpose programming language available everywhere", @"For students, learning Swift has been a great introduction to modern programming concepts and best practices. And because it is now open, their Swift skills will be able to be applied to an even broader range of platforms, from mobile devices to the desktop to the cloud.", @"Welcome to the Swift community. Together we are working to build a better programming language for everyone.", @"– The Swift Team"]; for (int i=0; i<30; i++) { NSInteger randomIndex = arc4random() % 5; FDDBaseCellModel *cellModel = [FDDBaseCellModel modelFromCellClass:HDTableViewCell.class cellHeight:[HDTableViewCell cellHeightWithCellData:randomSources[randomIndex]] cellData:randomSources[randomIndex]]; [self.dataArr addObject:cellModel]; } } - (void)disposeTableViewConverter{ _tableViewConverter = [[FDDTableViewConverter alloc] initWithTableViewCarrier:self daraSources:self.dataArr]; UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain]; tableView.delegate = _tableViewConverter; tableView.dataSource = _tableViewConverter; tableView.separatorStyle = UITableViewCellSeparatorStyleNone; [self.view addSubview:tableView]; } @end
Objective-C源碼地址
Swift源碼地址
歡迎 star