前言
UITableView動態行高一直是iOS的一個經典問題,在沒有AutoLayout的時代,只能自己計算frame,然后返回給代理,非常痛苦。到了AutoLayout的時代,布局就變得簡單多了,甚至于通過系統提供的API都能自動計算出行高。
UITableView+FDTemplateLayoutCell就是sunnyxx大大的一個自動計算行高的框架。只要布局正確,通過它可以自動計算并緩存行高,非常方便。不過在使用上發現一些問題,也嘗試去解決了。
過程
需求是這樣的,一個類似微博的頁面,像這樣:
這應該是比較經典的布局,內容和圖片都是不確定的,行高要根據實際數據計算。九宮格實現方式有很多,我這里是通過UICollectionView去實現的。這樣的一個好處就是UICollectionView的高度可以通過它的collectionViewLayout對象獲取,啥都不用算。不過會有一個問題,UICollectionView繼承自UIScrollView,它的高度沒法按照內容來全顯示。所以即使布局正確,通過AutoLayout來計算行高也是不包括UICollectionView的,這個問題同樣反映在一些UIView控件上。
這就十分蛋疼了,難道還要回到手算frame的時代?當然不是,是我還寫啥博客。
我說下解決的幾個方法。
方法一(不推薦):手動設置collectionView的高度,可以通過代碼或者xib來設置,我這里是xib。
像這樣手動指定collectionView的高度,然后賦值數據源的時候更新collectionView高度約束就可以了,讓它的高度等于它的contentSize.height,這樣就能全部顯示了,其它UIView控件也能這么解決。但是這樣在計算行高的時候會拋出非常多異常,都是約束的問題。我不是很清楚這是什么原因,按理說計算再后,賦值在前,應該不會這樣。而且顯示會出一些問題,計算的行高會不正確,有些許誤差。
方法二(推薦):既然不能通過這種方式,那就繞個彎吧。去掉高度約束,計算出來的高就不包含collectionView的高。然后再手動加上collectionView的高返回給代理不就行了。不過看下UITableView+FDTemplateLayoutCell的拓展方法:
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier
configuration:(void (^)(id cell))configuration;
只有一個設置cell數據源的block,正常情況下我們只需要把cell換成我們自己的類,然后賦值模型就行,緩存之類的框架會自動處理好。雖然我們可以獲取到緩存高度之后再加上collectionView的高,但是這樣還叫啥緩存,緩存就是不需要計算,直接取到就能用,那怎么辦呢?
雖然可以通過Method Swizzling黑魔法交換方法實現,但是這并不是最優方法,往往是一些莫名其妙的bug的源泉,作為開發者應該盡量避免這種方式。所以最后我選擇了通過分類的方式。思路是在框架計算完高度之后通過block返回,我們自行處理行高,加加減減,然后返回高度讓框架緩存。
具體代碼:
我們參考下框架這個方法的實現
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier
cacheByIndexPath:(NSIndexPath *)indexPath
configuration:(void (^)(id cell))configuration {
if (!identifier || !indexPath) {
return 0;
}
//命中緩存
if ([self.fd_indexPathHeightCache existsHeightAtIndexPath:indexPath]) {
[self fd_debugLog:[NSString stringWithFormat:@"hit cache by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @([self.fd_indexPathHeightCache heightForIndexPath:indexPath])]];
return [self.fd_indexPathHeightCache heightForIndexPath:indexPath];
}
//計算行高
CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
[self.fd_indexPathHeightCache cacheHeight:height byIndexPath:indexPath];
[self fd_debugLog:[NSString stringWithFormat: @"cached by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @(height)]];
return height;
}
它先從緩存中尋找行高,命中之后直接返回。否則計算行高,存入緩存,然后返回。所以很簡單,我們可以直接復制它的代碼。寫一個帶編輯行高功能的方法:
typedef CGFloat(^editCellHeightAction)(id cell, CGFloat cellHeight);
- (CGFloat)jh_heightForCellWithIdentifier:(NSString *)identifier
cacheByIndexPath:(NSIndexPath *)indexPath
configuration:(void (^)(id cell))configuration
editAction:(editCellHeightAction)editAction {
if (!identifier || !indexPath) {
return 0;
}
// Hit cache
if ([self.fd_indexPathHeightCache existsHeightAtIndexPath:indexPath]) {
[self fd_debugLog:[NSString stringWithFormat:@"hit cache by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @([self.fd_indexPathHeightCache heightForIndexPath:indexPath])]];
return [self.fd_indexPathHeightCache heightForIndexPath:indexPath];
}
CGFloat height = 0;
//獲取緩存中的cell
UITableViewCell *templateLayoutCell = [self fd_templateCellForReuseIdentifier:identifier];
//這里插入編輯行高的代碼
if (editAction) {
height = editAction(templateLayoutCell, [self fd_heightForCellWithIdentifier:identifier configuration:configuration]);
}
else {
height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
}
[self.fd_indexPathHeightCache cacheHeight:height byIndexPath:indexPath];
[self fd_debugLog:[NSString stringWithFormat: @"cached by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @(height)]];
return height;
}
}
使用起來像這樣:
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [tableView jh_heightForCellWithIdentifier:@"MineCell" cacheByIndexPath:indexPath configuration:^(HeSquareCell *cell) {
//正常賦值數據源
cell.model = self.model;
} editAction:^CGFloat(MineCell *cell, CGFloat cellHeight) {
//cellHeight是上面的block計算后回調過來的 所以直接加上額外的高度即可
//因為緩存的關系這里只會走一次 所以可以放心寫
return cellHeight + [cell collectionViewHeightWithModel:self.model];
}];
}
這樣高度就能正常顯示了,而且也不會拋異常,還能享受框架帶來的便利。
UITableView+FDTemplateLayoutCell的接口設計很易于拓展,所以寫起來很簡單。還有個問題,我發現在使用這個框架的時候,如果_tableView.tableFooterView = [[UIView alloc] init];
這句話寫在注冊cell之前,程序會crash,不造為啥。如果各位有更好的解決思路或者文中有錯誤的地方歡迎給我留言。