iOS 第三方庫計(jì)算cell高度 UITableView+FDTemplateLayoutCell 入門

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)入

GitHub轉(zhuǎn)送門??

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)行手動處理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容