UITableView+FDTemplateLayoutCell 是一個由國人團(tuán)隊(duì)開發(fā)的優(yōu)化計(jì)算 UITableViewCell 高度的輕量級框架(GitHub 地址?),由于實(shí)現(xiàn)邏輯簡明清晰,代碼也不復(fù)雜,非常適合作為新手學(xué)習(xí)其他著名卻龐大的開源項(xiàng)目的“入門教材”。
開發(fā)者之一的陽神也通過一篇博客介紹了 UITableViewCell 高度計(jì)算(尤其是 autoLayout 自動高度計(jì)算)的方方面面??偨Y(jié)一下的話就是:
iOS8 之前雖然采用 autoLayout 相比 frame layout 得手動計(jì)算已經(jīng)簡化了不少(設(shè)置estimatedRowHeight 屬性并對約束設(shè)置正確的 cell 的 contentView 執(zhí)行systemLayoutSizeFittingSize: 方法),但還是需要一些模式化步驟,同時還可能遇到一些蛋疼的問題比如 UILabel折行時的高度計(jì)算;
iOS8 推出 self-sizing cell 后,一切都變得輕松無比——做好約束后,直接設(shè)置estimatedRowHeight 就好了。然而事情并不簡單,一來我們依然需要做 iOS7 的適配,二來 self-sizing并不存在緩存機(jī)制,不論何時都會重新計(jì)算 cell 高度,導(dǎo)致 iOS8 下頁面滑動時會有明顯的卡頓。
因此,這個框架的目的,引用陽神的原話,就是“既有 iOS8 self-sizing 功能簡單的 API,又可以達(dá)到 iOS7 流暢的滑動效果,還保持了最低支持 iOS6”。
使用
一、cocoapods導(dǎo)入
pod 'UITableView+FDTemplateLayoutCell’
二、使用
1、引用 UITableView+FDTemplateLayoutCell.h 類;
2、如果是用代碼或 XIB 創(chuàng)建的 cell,需要先進(jìn)行注冊(類似 UICollectionView):
- (void)registerClass:(nullableClass)cellClassforCellReuseIdentifier:(NSString *)identifier;
- (void)registerNib:(nullableUINib *)nibforCellReuseIdentifier:(NSString *)identifier; ?//XIB
3、在 tableView: heightForRowAtIndexPath: 代理方法中調(diào)用以下三個方法之一完成高度獲?。?/p>
/*
identifier 即 cell 的 identifier;
configuration block 中的代碼應(yīng)與數(shù)據(jù)源方法 tableView: cellForRowAtIndexPath: 中對 cell 的設(shè)置代碼相同
方法內(nèi)部將根據(jù)以上兩個參數(shù)創(chuàng)建與 cell 對應(yīng)的 template layout cell,這個 cell 只進(jìn)行高度計(jì)算,不會顯示到屏幕上
*/
// 返回計(jì)算好的高度(無緩存)
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifierconfiguration:(void (^)(idcell))configuration;
// 返回計(jì)算好的高度,并根據(jù) indexPath 內(nèi)部創(chuàng)建與之相應(yīng)的二維數(shù)組緩存高度
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifiercacheByIndexPath:(NSIndexPath *)indexPathconfiguration:(void (^)(idcell))configuration;
// 返回計(jì)算好的高度,內(nèi)部創(chuàng)建一個字典緩存高度并由使用者指定 key
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifiercacheByKey:(id)keyconfiguration:(void (^)(idcell))configuration;
一般來說 cacheByIndexPath: 方法最為“傻瓜”,可以直接搞定所用問題。cacheByKey:方法稍顯復(fù)雜(需要關(guān)注數(shù)據(jù)刷新),但在緩存機(jī)制上相比 cacheByIndexPath:方法更為高效。因此,像類似微博、新聞這種會擁有唯一標(biāo)識的 cell 數(shù)據(jù)模型,更建議使用cacheByKey: 方法。
4、數(shù)據(jù)源變動時的緩存處理是個值得關(guān)注的問題。
對于 cacheByIndexPath: 方法,框架內(nèi)對 9 個觸發(fā) UITableView 刷新機(jī)制的公有方法分別進(jìn)行了處理,保證緩存數(shù)組的正確;同時,還提供了一個 UITableView 分類方法:
- (void)fd_reloadDataWithoutInvalidateIndexPathHeightCache;
用于需要刷新數(shù)據(jù)但不想移除原有緩存數(shù)據(jù)(框架內(nèi)對 reloadData 方法的處理是清空緩存)時調(diào)用,比如常見的“下拉加載更多數(shù)據(jù)”操作。
對于 cacheByKey: 方法,當(dāng) cell 高度發(fā)生改變時,必須手動處理:
// 移除 key 對應(yīng)的高度緩存
[tableView.fd_keyedHeightCacheinvalidateHeightForKey:key];
// 移除所有高度緩存
[tableView.fd_keyedHeightCacheinvalidateAllHeightCache];
5、如果需要查看 debug 打印信息,設(shè)置 fd_debugLogEnabled 屬性:
tableView.fd_debugLogEnabled =YES;
框架也為 UITableViewHeaderFooterView 設(shè)計(jì)了相應(yīng)方法,因?yàn)楹?UITableViewCell 相似。
框架分析
由于采用了分類機(jī)制,因此框架中大量使用 runtime 的關(guān)聯(lián)對象(Associated Object)進(jìn)行公有和私有變量的實(shí)現(xiàn),不了解的童鞋可以網(wǎng)上搜索一下相關(guān)概念。
結(jié)構(gòu)
框架提供了 4 個類,其中 UITableView+FDTemplateLayoutCellDebug 類用于打印 debug 信息,并無其它作用。主要功能由另外 3 個類提供。
UITableView+FDTemplateLayoutCell:主類,提供高度獲取方法;
UITableView+FDIndexPathHeightCache:創(chuàng)建了一個用于 cacheByIndexPath: 方法的緩存類 FDIndexPathHeightCache;
UITableView+FDKeyedHeightCache:創(chuàng)建了一個用于 cacheByKey: 方法的緩存類 FDKeyedHeightCache。
流程
我們直接以 cacheByIndexPath: 方法源碼為例進(jìn)行了解(cacheByKey: 方法的實(shí)現(xiàn)大同小異)。
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifiercacheByIndexPath:(NSIndexPath *)indexPathconfiguration:(void(^)(idcell))configuration {
// 1. 如果 identifier 和 indexPath 為空,返回高度為 0
if(!identifier || !indexPath) {
return0;? ?
}
// 2. 通過 FDIndexPathHeightCache 類聲明的方法檢查是否存在相應(yīng)緩存
if([self.fd_indexPathHeightCacheexistsHeightAtIndexPath:indexPath]) {
// 打印 debug 信息
[self fd_debugLog:[NSStringstringWithFormat:@"hit cache by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @([self.fd_indexPathHeightCacheheightForIndexPath:indexPath])]];
// 提取并返回對應(yīng)緩存中的額高度
return[self.fd_indexPathHeightCacheheightForIndexPath:indexPath];? ?
}
// 3. 如果沒有緩存,通過 fd_heightForCellWithIdentifier: configuration: 方法計(jì)算獲得 cell 高度
CGFloatheight = [selffd_heightForCellWithIdentifier:identifierconfiguration:configuration];
// 4. 通過 FDIndexPathHeightCache 類聲明的方法將高度存入緩存
[self.fd_indexPathHeightCache cacheHeight:heightbyIndexPath:indexPath];
// 打印 debug 信息
[self fd_debugLog:[NSStringstringWithFormat:@"cached by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @(height)]];
returnheight;
}
高度計(jì)算
fd_heightForCellWithIdentifier: configuration: 方法會根據(jù) identifier 以及configuration block 提供一個和 cell 布局相同的 template layout cell,并將其傳入fd_systemFittingHeightForConfiguratedCell: 這個私有方法返回計(jì)算出的高度。
關(guān)于 UILabel 的折行
框架的做法相當(dāng)直接:獲取當(dāng)前 contentView 的寬度并添加為其約束,限制 UILabel 水平方向的展開,計(jì)算完成后移除。
獲取當(dāng)前 contentView 的寬度:
CGFloatcontentViewWidth=CGRectGetWidth(self.frame);
// 考慮存在 accessoryView 或者 accessoryType 的情況
if(cell.accessoryView) {? ?
contentViewWidth -=16+CGRectGetWidth(cell.accessoryView.frame);
}else{
staticconstCGFloatsystemAccessoryWidths[] = {? ? ? ? [UITableViewCellAccessoryNone] =0,? ? ? ? [UITableViewCellAccessoryDisclosureIndicator] =34,? ? ? ? [UITableViewCellAccessoryDetailDisclosureButton] =68,? ? ? ? [UITableViewCellAccessoryCheckmark] =40,? ? ? ? [UITableViewCellAccessoryDetailButton] =48
};? ?
contentViewWidth -= systemAccessoryWidths[cell.accessoryType];
}
添加約束并進(jìn)行高度計(jì)算:
// 添加約束
NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nilattribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth];
[cell.contentView addConstraint:widthFenceConstraint];
// 計(jì)算高度
fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
// 移除約束
[cell.contentView removeConstraint:widthFenceConstraint];
注意分格線高度,這也是非常容易遺漏的一點(diǎn):
// Add1px extra spaceforseparator lineifneeded, simulatingdefaultUITableViewCell.
if(self.separatorStyle != UITableViewCellSeparatorStyleNone) {???
fittingHeight +=1.0/ [UIScreenmainScreen].scale;
}
緩存 FDIndexPathHeightCache
外部接口:
// 當(dāng)前 indexPath 是否存在緩存
-(BOOL)existsHeightAtIndexPath:(NSIndexPath*)indexPath;
// 存入緩存
-(void)cacheHeight:(CGFloat)heightbyIndexPath:(NSIndexPath*)indexPath;
// 從緩存讀取高度
-(CGFloat)heightForIndexPath:(NSIndexPath*)indexPath;
// 移除指定 indexPath 的緩存
-(void)invalidateHeightAtIndexPath:(NSIndexPath*)indexPath;
// 移除所有緩存
-(void)invalidateAllHeightCache;
其內(nèi)部針對橫屏和豎屏聲明了 2 個以 indexPath 為索引的二維數(shù)組來存儲高度:
typedef NSMutableArray<NSMutableArray<>NSnumber *>*> FDIndexPathHeightsBySection;
@interfaceFDIndexPathHeightCache()
@property(nonatomic,strong) FDIndexPathHeightsBySection *heightsBySectionForPortrait;
@property(nonatomic,strong) FDIndexPathHeightsBySection *heightsBySectionForLandscape;
@end
更新處理
框架聲明了一個 tableView 分類 UITableView(FDIndexPathHeightCacheInvalidation),利用 runtime 的method_exchangeImplementations 函數(shù)對 UITableView 中觸發(fā)刷新的方法做了替換,以進(jìn)行相應(yīng)的緩存調(diào)整:
@implementationUITableView(FDIndexPathHeightCacheInvalidation)
+ (void)load {
// UITableView 中所有觸發(fā)刷新的公共方法
SELselectors[] = {
@selector(reloadData),
@selector(insertSections:withRowAnimation:),
@selector(deleteSections:withRowAnimation:),
@selector(reloadSections:withRowAnimation:),
@selector(moveSection:toSection:),
@selector(insertRowsAtIndexPaths:withRowAnimation:),
@selector(deleteRowsAtIndexPaths:withRowAnimation:),
@selector(reloadRowsAtIndexPaths:withRowAnimation:),
@selector(moveRowAtIndexPath:toIndexPath:)? ?
};
// 用分類中以“fd_”為前綴的方法替換
for(NSUIntegerindex =0; index < sizeof(selectors) / sizeof(SEL); ++index) {? ? ? ?
SELoriginalSelector = selectors[index];? ? ? ?
SELswizzledSelector = NSSelectorFromString([@"fd_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
MethodoriginalMethod = class_getInstanceMethod(self, originalSelector);? ? ? ?
MethodswizzledMethod = class_getInstanceMethod(self, swizzledSelector);? ? ? ?
method_exchangeImplementations(originalMethod, swizzledMethod);? ?
}
}
FDKeyedHeightCache
相比于 FDIndexPathHeightCache 中較為繁瑣的數(shù)組操作,F(xiàn)DKeyedHeightCache 顯得簡潔了許多(當(dāng)然代價是高度變化時的緩存操作得使用者親力親為)。外部接口:
- (BOOL)existsHeightForKey:(id)key;
- (void)cacheHeight:(CGFloat)heightbyKey:(id)key;
- (CGFloat)heightForKey:(id)key;
// Invalidation
- (void)invalidateHeightForKey:(id)key;
- (void)invalidateAllHeightCache;
內(nèi)部采用以 key 為索引的字典存儲高度:
@interfaceFDKeyedHeightCache()
@property(nonatomic,strong)NSMutableDictionary<id<NSCpying>,NSNumber*> *mutableHeightsByKeyForPortrait;
@property(nonatomic,strong)NSMutableDictionary<id<NsCopying>,NSNumber*> *mutableHeightsByKeyForLandscape;
@end
由于采用字典緩存,自然不用關(guān)心 cell 插入、刪除、移動等造成的緩存數(shù)組排列問題,但是當(dāng) cell 高度發(fā)生改變時,我們也無法像數(shù)組那樣根據(jù) IndexPath 索引到對應(yīng)的緩存,因此只能像上文“使用”部分說明的一樣,進(jìn)行手動處理。