供外部调用的核心方法
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
FDSimulatedCacheMode mode = self.cacheModeSegmentControl.selectedSegmentIndex;
switch (mode) {
case FDSimulatedCacheModeNone:
return [tableView fd_heightForCellWithIdentifier:@"FDFeedCell" configuration:^(FDFeedCell *cell) {
[self configureCell:cell atIndexPath:indexPath];
}];
case FDSimulatedCacheModeCacheByIndexPath:
return [tableView fd_heightForCellWithIdentifier:@"FDFeedCell" cacheByIndexPath:indexPath configuration:^(FDFeedCell *cell) {
[self configureCell:cell atIndexPath:indexPath];
}];
case FDSimulatedCacheModeCacheByKey: {
FDFeedEntity *entity = self.feedEntitySections[indexPath.section][indexPath.row];
return [tableView fd_heightForCellWithIdentifier:@"FDFeedCell" cacheByKey:entity.identifier configuration:^(FDFeedCell *cell) {
[self configureCell:cell atIndexPath:indexPath];
}];
};
default:
break;
}
}
方法一是没有缓存高度,每次都需要调用以下代码进行重新计算
UITableViewCell *templateLayoutCell = [self fd_templateCellForReuseIdentifier:identifier];
return [self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell];
方法二和三则是缓存高度,其中cacheByIndexPath和cacheByKey:entity只是根据不同的key进行缓存
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration {
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 = [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)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id<NSCopying>)key configuration:(void (^)(id cell))configuration {
if (!identifier || !key) {
return 0;
}
// Hit cache
if ([self.fd_keyedHeightCache existsHeightForKey:key]) {
CGFloat cachedHeight = [self.fd_keyedHeightCache heightForKey:key];
[self fd_debugLog:[NSString stringWithFormat:@"hit cache by key[%@] - %@", key, @(cachedHeight)]];
return cachedHeight;
}
CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
[self.fd_keyedHeightCache cacheHeight:height byKey:key];
[self fd_debugLog:[NSString stringWithFormat:@"cached by key[%@] - %@", key, @(height)]];
return height;
}
代码先根据cached进行查找,如果存在则直接返回该高度
如果不存在,调用方法进行高度计算得出最后需要返回的结果,同时用cached进行高度的缓存,上述两种方法只是通过不同的cached进行查找和缓存高度,其实现机制是完全一样的
内部缓存的实现
cachedIndexPath和cachedKey两种方法进行缓存,两种方法实现原理一样,只不过采用的缓存容器不同
1.cachedKey
核心方法如下
if ([self.fd_keyedHeightCache existsHeightForKey:key]) {
CGFloat cachedHeight = [self.fd_keyedHeightCache heightForKey:key];
}
CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
[self.fd_keyedHeightCache cacheHeight:height byKey:key];
UITableView+FDKeyedHeightCache.m类中的两个字典是用来存储高度的容器
@property (nonatomic, strong) NSMutableDictionary<id<NSCopying>, NSNumber *> *mutableHeightsByKeyForPortrait;
@property (nonatomic, strong) NSMutableDictionary<id<NSCopying>, NSNumber *> *mutableHeightsByKeyForLandscape;
步骤:
、通过key判断是否存在对应的高度缓存值,内部实现为字典通过key获得对应的value,对value值判断并返回bool值
、如果存在,返回true,此时可以通过key找到value并返回该value
、如果不存在,返回false,此时需要先调用方法heightForCellWithIdentifier:identifier configuration:configuration对高度进行计算,然后调用方法cacheHeight:height byKey:key
进行字典的键值对设置即高度的存储,并最终返回计算得出的高度
2.cachedIndexPath
核心方法如下
if ([self.fd_indexPathHeightCache existsHeightAtIndexPath:indexPath]) {
return [self.fd_indexPathHeightCache heightForIndexPath:indexPath];
}
CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
[self.fd_indexPathHeightCache cacheHeight:height byIndexPath:indexPath];
UITableView+FDIndexPathHeightCache.m类中的两个数组是用来存储高度的容器,这两个数组都是二维数组,根据indexPath的section和row进行存储
typedef NSMutableArray<NSMutableArray<NSNumber *> *> FDIndexPathHeightsBySection;
@property (nonatomic, strong) FDIndexPathHeightsBySection *heightsBySectionForPortrait;
@property (nonatomic, strong) FDIndexPathHeightsBySection *heightsBySectionForLandscape;
步骤:
、通过indexPath的section和row去二维数组中查找元素,对元素进行值的判断existsHeightAtIndexPath:indexPath
、如果有值,则说明有缓存高度,则调用方法heightForIndexPath:indexPath获得值并返回
、如果没有值,则先计算高度heightForCellWithIdentifier:identifier configuration:configuration,然后缓存cacheHeight:height byIndexPath:indexPath,最后返回该高度
高度计算的核心方法
从上面我们可以得出,如果有高度缓存则取出缓存,如果没有高度缓存则需要计算高度,然后进行缓存,计算高度的核心代码为
- (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell {
CGFloat contentViewWidth = CGRectGetWidth(self.frame);
CGRect cellBounds = cell.bounds;
cellBounds.size.width = contentViewWidth;
cell.bounds = cellBounds;
CGFloat accessroyWidth = 0;
// If a cell has accessory view or system accessory type, its content view's width is smaller
// than cell's by some fixed values.
if (cell.accessoryView) {
accessroyWidth = 16 + CGRectGetWidth(cell.accessoryView.frame);
} else {
static const CGFloat systemAccessoryWidths[] = {
[UITableViewCellAccessoryNone] = 0,
[UITableViewCellAccessoryDisclosureIndicator] = 34,
[UITableViewCellAccessoryDetailDisclosureButton] = 68,
[UITableViewCellAccessoryCheckmark] = 40,
[UITableViewCellAccessoryDetailButton] = 48
};
accessroyWidth = systemAccessoryWidths[cell.accessoryType];
}
contentViewWidth -= accessroyWidth;
// If not using auto layout, you have to override "-sizeThatFits:" to provide a fitting size by yourself.
// This is the same height calculation passes used in iOS8 self-sizing cell's implementation.
//
// 1. Try "- systemLayoutSizeFittingSize:" first. (skip this step if 'fd_enforceFrameLayout' set to YES.)
// 2. Warning once if step 1 still returns 0 when using AutoLayout
// 3. Try "- sizeThatFits:" if step 1 returns 0
// 4. Use a valid height or default row height (44) if not exist one
CGFloat fittingHeight = 0;
if (!cell.fd_enforceFrameLayout && contentViewWidth > 0) {
// Add a hard width constraint to make dynamic content views (like labels) expand vertically instead
// of growing horizontally, in a flow-layout manner.
NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth];
// [bug fix] after iOS 10.3, Auto Layout engine will add an additional 0 width constraint onto cell's content view, to avoid that, we add constraints to content view's left, right, top and bottom.
static BOOL isSystemVersionEqualOrGreaterThen10_2 = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
isSystemVersionEqualOrGreaterThen10_2 = [UIDevice.currentDevice.systemVersion compare:@"10.2" options:NSNumericSearch] != NSOrderedAscending;
});
NSArray<NSLayoutConstraint *> *edgeConstraints;
if (isSystemVersionEqualOrGreaterThen10_2) {
// To avoid confilicts, make width constraint softer than required (1000)
widthFenceConstraint.priority = UILayoutPriorityRequired - 1;
// Build edge constraints
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0];
NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeRight multiplier:1.0 constant:accessroyWidth];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];
NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0];
edgeConstraints = @[leftConstraint, rightConstraint, topConstraint, bottomConstraint];
[cell addConstraints:edgeConstraints];
}
[cell.contentView addConstraint:widthFenceConstraint];
// Auto layout engine does its math
fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
// Clean-ups
[cell.contentView removeConstraint:widthFenceConstraint];
if (isSystemVersionEqualOrGreaterThen10_2) {
[cell removeConstraints:edgeConstraints];
}
[self fd_debugLog:[NSString stringWithFormat:@"calculate using system fitting size (AutoLayout) - %@", @(fittingHeight)]];
}
if (fittingHeight == 0) {
#if DEBUG
// Warn if using AutoLayout but get zero height.
if (cell.contentView.constraints.count > 0) {
if (!objc_getAssociatedObject(self, _cmd)) {
NSLog(@"[FDTemplateLayoutCell] Warning once only: Cannot get a proper cell height (now 0) from '- systemFittingSize:'(AutoLayout). You should check how constraints are built in cell, making it into 'self-sizing' cell.");
objc_setAssociatedObject(self, _cmd, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
#endif
// Try '- sizeThatFits:' for frame layout.
// Note: fitting height should not include separator view.
fittingHeight = [cell sizeThatFits:CGSizeMake(contentViewWidth, 0)].height;
[self fd_debugLog:[NSString stringWithFormat:@"calculate using sizeThatFits - %@", @(fittingHeight)]];
}
// Still zero height after all above.
if (fittingHeight == 0) {
// Use default row height.
fittingHeight = 44;
}
// Add 1px extra space for separator line if needed, simulating default UITableViewCell.
if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
fittingHeight += 1.0 / [UIScreen mainScreen].scale;
}
return fittingHeight;
}
步骤:
、计算出用来适配的宽度,需要判断是否存在自定义的accessoryView或者系统的accessoryType,如果存在则宽度需要相应缩减
、根据cell.fd_enforceFrameLayout进行判断,默认为false,则是使用系统自带的方法[cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height进行高度的自适应得出一个高度,如果该高度为0那么会调用sizeThatFits进行高度的适应,如果是true则不会调用系统方法,需要重写sizeThatFits方法返回一个自己计算出的高度,如若经过上述步骤还是未获得高度则默认返回44的高度