UITableView+FDTemplateLayoutCell 源碼探究

UITableView+FDTemplateLayoutCell 源碼探究


  • 在我們?nèi)粘5臉I(yè)務(wù)中,常常伴隨大量的UITableView,然而動(dòng)態(tài)地計(jì)算Cell的高度常常困擾著我。自從使用了這個(gè)組件之后,一切都變得沒那么復(fù)雜。所以深入學(xué)習(xí)下這個(gè)框架的組件的實(shí)現(xiàn)原理。
  • 框架地址:https://github.com/forkingdog/UITableView-FDTemplateLayoutCell

代碼文件目錄

- UITableView+FDIndexPathHeightCache.h
- UITableView+FDIndexPathHeightCache.m
- UITableView+FDKeyedHeightCache.h
- UITableView+FDKeyedHeightCache.m
- UITableView+FDTemplateLayoutCell.h
- UITableView+FDTemplateLayoutCell.m
- UITableView+FDTemplateLayoutCellDebug.h
- UITableView+FDTemplateLayoutCellDebug.m

首先,介紹一下這幾個(gè)類的基本功能,再層層推進(jìn),逐一分析。

- UITableView+FDIndexPathHeightCache,主要負(fù)責(zé)cell通過NSIndexPath進(jìn)行緩存高度的功能
- UITableView+FDKeyedHeightCache,主要負(fù)責(zé)cell通過key值進(jìn)行緩存高度的功能
- UITableView+FDTemplateLayoutCell,提供接口方法方便用戶定義cell的數(shù)據(jù)源,以及幫助我們計(jì)算cell的高度
- UITableView+FDTemplateLayoutCellDebug,提供一些Debug打印信息

關(guān)于這個(gè)框架,坦白說,從代碼中看,作者無疑秀了一波runtime底層的功底,讓我這種小白起初一臉懵逼。自然我得換種思路來解讀這個(gè)框架,那就是從字?jǐn)?shù)最少的類入手吧。

UITableView+FDTemplateLayoutCellDebug

@interface UITableView (FDTemplateLayoutCellDebug)

//設(shè)置Debug模式是否打開
@property (nonatomic, assign) BOOL fd_debugLogEnabled;

//通過該方法,傳遞NSLog打印對(duì)應(yīng)的Debug信息
- (void)fd_debugLog:(NSString *)message;

@end
@implementation UITableView (FDTemplateLayoutCellDebug)

- (BOOL)fd_debugLogEnabled {
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

- (void)setFd_debugLogEnabled:(BOOL)debugLogEnabled {
    objc_setAssociatedObject(self, @selector(fd_debugLogEnabled), @(debugLogEnabled), OBJC_ASSOCIATION_RETAIN);
}

- (void)fd_debugLog:(NSString *)message {
    if (self.fd_debugLogEnabled) {
        NSLog(@"** FDTemplateLayoutCell ** %@", message);
    }
}

@end
  • 在分類中,如果要聲明屬性,可以通過使用關(guān)聯(lián)度對(duì)象( AssociatedObject ), 通過objc_setAssociatedObject() 添加屬性,objc_getAssociatedObject() 獲取屬性。實(shí)際上,相當(dāng)于在運(yùn)行時(shí)系統(tǒng)中動(dòng)態(tài)地在內(nèi)存中開辟一塊空間,存儲(chǔ)debugLogEnabled這個(gè)BOOL變量,類似懶加載的方式,通過runtime實(shí)現(xiàn)setter & getter方法。
  • 關(guān)于runtime的知識(shí)點(diǎn),推薦這篇博客:http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/

UITableView+FDKeyedHeightCache

#import <UIKit/UIKit.h>

@interface FDKeyedHeightCache : NSObject

//判斷緩存中是否存在key為值的緩存高度
- (BOOL)existsHeightForKey:(id<NSCopying>)key;

//對(duì)指定key的cell設(shè)置高度為height
- (void)cacheHeight:(CGFloat)height byKey:(id<NSCopying>)key;

//從緩存中獲取對(duì)應(yīng)key的cell的高度height值
- (CGFloat)heightForKey:(id<NSCopying>)key;

//從緩存中刪除指定key的cell的值
- (void)invalidateHeightForKey:(id<NSCopying>)key;

//移除緩存中所有的cell的高度緩存值
- (void)invalidateAllHeightCache;
@end

@interface UITableView (FDKeyedHeightCache)
@property (nonatomic, strong, readonly) FDKeyedHeightCache *fd_keyedHeightCache;
@end
  • 先來看看FDKeyedHeightCache類中聲明的屬性
@property (nonatomic, strong) NSMutableDictionary<id<NSCopying>, NSNumber *> *mutableHeightsByKeyForPortrait;

@property (nonatomic, strong) NSMutableDictionary<id<NSCopying>, NSNumber *> *mutableHeightsByKeyForLandscape;

不難看出,這是兩個(gè)指定泛型的可變字典。

  • mutableHeightsByKeyForPortrait : 用于緩存設(shè)備豎直放置時(shí),對(duì)應(yīng)key的cell的高度值。
  • mutableHeightsByKeyForLandscape : 用于緩存設(shè)備橫向放置時(shí),對(duì)應(yīng)key的cell的高度值。

  • FDKeyedHeightCache中的接口方法
- (BOOL)existsHeightForKey:(id<NSCopying>)key {
    NSNumber *number = self.mutableHeightsByKeyForCurrentOrientation[key];
    return number && ![number isEqualToNumber:@-1];
}

- (void)cacheHeight:(CGFloat)height byKey:(id<NSCopying>)key {
    self.mutableHeightsByKeyForCurrentOrientation[key] = @(height);
}

- (CGFloat)heightForKey:(id<NSCopying>)key {
#if CGFLOAT_IS_DOUBLE
    return [self.mutableHeightsByKeyForCurrentOrientation[key] doubleValue];
#else
    return [self.mutableHeightsByKeyForCurrentOrientation[key] floatValue];
#endif
}

- (void)invalidateHeightForKey:(id<NSCopying>)key {
    [self.mutableHeightsByKeyForPortrait removeObjectForKey:key];
    [self.mutableHeightsByKeyForLandscape removeObjectForKey:key];
}

- (void)invalidateAllHeightCache {
    [self.mutableHeightsByKeyForPortrait removeAllObjects];
    [self.mutableHeightsByKeyForLandscape removeAllObjects];
}
  • 這些方法并不晦澀,看到這里,大家不禁會(huì)問,self.mutableHeightsByKeyForCurrentOrientation從何而來,這也是我覺得這個(gè)類中,細(xì)節(jié)處理比較好的地方,由于此處考慮到緩存的高度區(qū)別了設(shè)備方向,所以框架作者,通過一個(gè)getter方法來獲取對(duì)應(yīng)的存放高度的字典。
- (NSMutableDictionary<id<NSCopying>, NSNumber *> *)mutableHeightsByKeyForCurrentOrientation {
    return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation) ? self.mutableHeightsByKeyForPortrait: self.mutableHeightsByKeyForLandscape;
}
  • 根據(jù)UIDeviceOrientationIsPortrait()函數(shù),傳入當(dāng)前設(shè)備的放置方向([UIDevice currentDevice].orientation

    )進(jìn)行判斷。從而便可以通過屬性簡潔判斷需要從那個(gè)字典中取值了。


UITableView+FDIndexPathHeightCache

@interface FDIndexPathHeightCache : NSObject

// 如果您使用索引路徑獲取高度緩存,則自動(dòng)啟用
@property (nonatomic, assign) BOOL automaticallyInvalidateEnabled;

// Height cache
- (BOOL)existsHeightAtIndexPath:(NSIndexPath *)indexPath;
- (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)heightForIndexPath:(NSIndexPath *)indexPath;
- (void)invalidateHeightAtIndexPath:(NSIndexPath *)indexPath;
- (void)invalidateAllHeightCache;

@end

@interface UITableView (FDIndexPathHeightCache)
@property (nonatomic, strong, readonly) FDIndexPathHeightCache *fd_indexPathHeightCache;
@end

@interface UITableView (FDIndexPathHeightCacheInvalidation)
/// 當(dāng)你不想通過刪除緩存中的高度來刷新數(shù)據(jù)源重新計(jì)算時(shí),可以調(diào)用這個(gè)方法。
/// 該方法中用過runtime重寫了tableView中修改cell的一些方法,例如插入cell,刪除cell,移動(dòng)cell,以及reloadData方法。
- (void)fd_reloadDataWithoutInvalidateIndexPathHeightCache;
@end
  • 首先看看FDIndexPathHeightCache中設(shè)置的屬性
typedef NSMutableArray<NSMutableArray<NSNumber *> *> FDIndexPathHeightsBySection;

@interface FDIndexPathHeightCache ()
@property (nonatomic, strong) FDIndexPathHeightsBySection *heightsBySectionForPortrait;
@property (nonatomic, strong) FDIndexPathHeightsBySection *heightsBySectionForLandscape;
@end

通過前面key的高度緩存分析,不難猜出這幾個(gè)屬性是干什么的。

  • 由于通過NSIndexPath獲取高度緩存,NSIndexPath對(duì)應(yīng)section, 以及indexPath。FDIndexPathHeightsBySection這個(gè)數(shù)組,通過數(shù)組嵌套字典的數(shù)據(jù)結(jié)構(gòu)來存儲(chǔ),不同的section組中對(duì)應(yīng)的cell的高度緩存。

  • FDIndexPathHeightCache中的方法

    由于頭文件聲明的幾個(gè)接口方法,與FDKeyedHeightCache中的思路類似,就不再費(fèi)口舌了,大家翻看源碼便一目了然。

- (void)enumerateAllOrientationsUsingBlock:(void (^)(FDIndexPathHeightsBySection *heightsBySection))block {
    block(self.heightsBySectionForPortrait);
    block(self.heightsBySectionForLandscape);
}

- (void)invalidateHeightAtIndexPath:(NSIndexPath *)indexPath {
    [self buildCachesAtIndexPathsIfNeeded:@[indexPath]];
    [self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection {
        heightsBySection[indexPath.section][indexPath.row] = @-1;
    }];
}

- (void)invalidateAllHeightCache {
    [self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) {
        [heightsBySection removeAllObjects];
    }];
}

- (void)buildCachesAtIndexPathsIfNeeded:(NSArray *)indexPaths {
    // Build every section array or row array which is smaller than given index path.
    [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
        [self buildSectionsIfNeeded:indexPath.section];
        [self buildRowsIfNeeded:indexPath.row inExistSection:indexPath.section];
    }];
}

- (void)buildSectionsIfNeeded:(NSInteger)targetSection {
    [self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) {
        for (NSInteger section = 0; section <= targetSection; ++section) {
            if (section >= heightsBySection.count) {
                heightsBySection[section] = [NSMutableArray array];
            }
        }
    }];
}

- (void)buildRowsIfNeeded:(NSInteger)targetRow inExistSection:(NSInteger)section {
    [self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) {
        NSMutableArray<NSNumber *> *heightsByRow = heightsBySection[section];
        for (NSInteger row = 0; row <= targetRow; ++row) {
            if (row >= heightsByRow.count) {
                heightsByRow[row] = @-1;
            }
        }
    }];
}

  • 這幾個(gè)封裝的方法,主要一點(diǎn)就是通過block來回調(diào),判斷刪除NSIndexPath對(duì)應(yīng)的cell高度緩存。

  • 在這個(gè)類中,最核心的莫過于UITableView (FDIndexPathHeightCacheInvalidation) 這個(gè)分類的實(shí)現(xiàn)細(xì)節(jié),廢話少說,繼續(xù)看代碼。
//我們只是轉(zhuǎn)發(fā)主調(diào)用,在崩潰報(bào)告中,最頂層的方法在堆棧可能存在FD,
//但它真的不是我們的錯(cuò)誤,你應(yīng)該檢查你的表視圖的數(shù)據(jù)源和
//重新加載時(shí)顯示單元格不匹配。
static void __FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(void (^callout)(void)) {
    callout();
}
#define FDPrimaryCall(...) do {__FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(^{__VA_ARGS__});} while(0)
  • 調(diào)用的接口方法
- (void)fd_reloadDataWithoutInvalidateIndexPathHeightCache {
    FDPrimaryCall([self fd_reloadData];);
}
  • 這個(gè)方法,主要調(diào)用的是[self fd_reloadData],看到這里的時(shí)候,我們的第一反應(yīng)應(yīng)該是此處通過runtime 交換了系統(tǒng)方法的實(shí)現(xiàn)。這是一種動(dòng)態(tài)的攔截技巧,也算是基礎(chǔ)的runtime知識(shí)了,懵逼的小伙伴可以認(rèn)真閱讀下前面提到的關(guān)于runtime的大牛博文。

  • 既然如此,先來看看作者重寫了哪些系統(tǒng)的方法吧。
+ (void)load {
    // All methods that trigger height cache's invalidation
    SEL selectors[] = {
        @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:)
    };
    
    for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
        SEL originalSelector = selectors[index];
        SEL swizzledSelector = NSSelectorFromString([@"fd_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
        Method originalMethod = class_getInstanceMethod(self, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
  • 通過method_exchangeImplementations() C函數(shù), 將重寫的方法,一一交換成重寫的方法。
  • 在這些fd_方法中的實(shí)現(xiàn)細(xì)節(jié)中,需要注意的一點(diǎn)就是,如果對(duì)應(yīng)的fd_indexPathHeightCache設(shè)置了automaticallyInvalidateEnabled屬性為YES時(shí),對(duì)應(yīng)的方法對(duì)高度緩存做相應(yīng)的處理,重新更新fd_indexPathHeightCache中存儲(chǔ)的高度緩存。
  • 當(dāng)?shù)谝淮蝦eloadData,或者cell的行數(shù)發(fā)生變化(增減行,section) ,會(huì)先在tableview不處于滾動(dòng)狀態(tài)的時(shí)候異步計(jì)算那些沒有被計(jì)算過的cell的高度,做預(yù)緩存,這個(gè)想法非常贊。
  • 使用者需要小心,這些調(diào)用是異步的, tableview delegate有可能會(huì)在預(yù)緩存計(jì)算的時(shí)候不存在了,導(dǎo)致程序崩潰,所以使用者在tableview需要析構(gòu)的時(shí)候,在對(duì)應(yīng)的tableview controller的dealloc中講self.tableview.delegate = nil;,確保delegate后續(xù)不會(huì)是一個(gè)野指針。

UITableView+FDTemplateLayoutCell

至此,我們已經(jīng)分析了幾個(gè)子類的實(shí)現(xiàn)邏輯,唯一剩下一個(gè)分類,也是我們使用這個(gè)框架的入口 FDTemplateLayoutCell分類。全面了解這個(gè)組件近在咫尺。

@interface UITableView (FDTemplateLayoutCell)

/* 為給定的重用標(biāo)識(shí)符訪問內(nèi)部模板布局單元格。
 * 一般來說,你不需要知道這些模板布局單元格。
 * @param identifier重用必須注冊(cè)的單元格的標(biāo)識(shí)符。
*/ 
- (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier;

/* 返回由重用標(biāo)識(shí)符指定并配置的類型的單元格的高度, 并通過block來配置。
 * 單元格將被放置在固定寬度,垂直擴(kuò)展的基礎(chǔ)上,相對(duì)于其動(dòng)態(tài)內(nèi)容,使用自動(dòng)布局。 
 * 因此,這些必要的單元被設(shè)置為自適應(yīng),即其內(nèi)容總是確定它的寬度給定的寬度等于tableview的寬度。
 * @param identifier用于檢索和維護(hù)模板的字符串標(biāo)識(shí)符cell通過系統(tǒng)方法
 * '- dequeueReusableCellWithIdentifier:'
 * @param configuration用于配置和提供內(nèi)容的可選塊到模板單元格。 
 * 配置應(yīng)該是最小的滾動(dòng)性能足以計(jì)算單元格的高度。
*/
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration;

/* 計(jì)算的高度將通過其索引路徑進(jìn)行高速緩存,當(dāng)需要時(shí)返回高速緩存的高度,因此,可以節(jié)省大量額外的高度計(jì)算。
 * 無需擔(dān)心數(shù)據(jù)源更改時(shí)使緩存高度無效,它將在調(diào)用“-reloadData”或任何觸發(fā)方法時(shí)自動(dòng)完成UITableView的重新加載。
 * @param indexPath此單元格的高度緩存所屬的位置。
*/
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration;

/* 此方法通過模型實(shí)體的標(biāo)識(shí)符緩存高度。
 * 如果你的模型改變,調(diào)用“-invalidateHeightForKey:(id <NSCopying>)key”到無效緩存并重新計(jì)算,它比“cacheByIndexPath”方便得多。
 * @param key model entity的標(biāo)識(shí)符,其數(shù)據(jù)配置一個(gè)單元格。
*/ 
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id<NSCopying>)key configuration:(void (^)(id cell))configuration;

@end

@interface UITableView (FDTemplateLayoutHeaderFooterView)

/* 返回在具有重用標(biāo)識(shí)符的表視圖中注冊(cè)的Header或Footer視圖的高度。
 * 在調(diào)用“ - [UITableView registerNib / Class:forHeaderFooterViewReuseIdentifier]”之后使用它,
 * 與“-fd_heightForCellWithIdentifier:configuration:”相同。
 * 它將調(diào)用“-sizeThatFits:”
 * UITableViewHeaderFooterView的子類不使用自動(dòng)布局。
*/ 
- (CGFloat)fd_heightForHeaderFooterViewWithIdentifier:(NSString *)identifier configuration:(void (^)(id headerFooterView))configuration;

@end

@interface UITableViewCell (FDTemplateLayoutCell)
/* 指示這是僅用于計(jì)算的模板布局單元格。
 * 當(dāng)配置單元格時(shí),如果有非UI的副作用,你可能需要這個(gè)。
 * 類似:
 *   - (void)configureCell:(FooCell *)cell atIndexPath:(NSIndexPath *)indexPath {
 *       cell.entity = [self entityAtIndexPath:indexPath];
 *       if (!cell.fd_isTemplateLayoutCell) {
 *           [self notifySomething]; // non-UI side effects
 *       }
 *   }
*/
@property (nonatomic, assign) BOOL fd_isTemplateLayoutCell;

/* 啟用以強(qiáng)制此模板布局單元格使用“框架布局”而不是“自動(dòng)布局”,
 * 并且通過調(diào)用“-sizeThatFits:”來詢問單元格的高度,所以你必須重寫這個(gè)方法。
 * 僅當(dāng)要手動(dòng)控制此模板布局單元格的高度時(shí)才使用此屬性
 * 計(jì)算模式,默認(rèn)為NO。
*/
@property (nonatomic, assign) BOOL fd_enforceFrameLayout;

@end

  • 先來看看我們平時(shí)開發(fā)中最頻繁調(diào)用的兩個(gè)方法
  • (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration;
  • (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id<NSCopying>)key configuration:(void (^)(id cell))configuration;
- (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]) {
        return [self.fd_indexPathHeightCache heightForIndexPath:indexPath];
    }
    
    CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
    [self.fd_indexPathHeightCache cacheHeight:height byIndexPath:indexPath];

    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];
        return cachedHeight;
    }
    
    CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
    [self.fd_keyedHeightCache cacheHeight:height byKey:key];
    return height;
}


  • 這兩個(gè)方法,分別是對(duì)cell通過NSIndexPath 或者 key值 進(jìn)行高度緩存,讀取高度的時(shí)候,先從緩存cache中讀取,如果緩存中沒有,在通過[self fd_heightForCellWithIdentifier:identifier configuration:configuration]方法進(jìn)行計(jì)算高度并加入緩存中。
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration {
    if (!identifier) {
        return 0;
    }
    
    UITableViewCell *templateLayoutCell = [self fd_templateCellForReuseIdentifier:identifier];
    
    //手動(dòng)調(diào)用以確保與實(shí)際單元格的一致行為。 (顯示在屏幕上)
    [templateLayoutCell prepareForReuse];
    
    if (configuration) {
        configuration(templateLayoutCell);
    }
    
    return [self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell];
}
  • 通過blocks進(jìn)行配置并計(jì)算cell的高度,主要通過[self fd_templateCellForReuseIdentifier:identifier]方法創(chuàng)建一個(gè)UITableViewCell的實(shí)例templateLayoutCell,最后再把templateLayoutCell放入[self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell]中進(jìn)行計(jì)算返回高度。
- (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier {
    NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
    
    NSMutableDictionary<NSString *, UITableViewCell *> *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
    if (!templateCellsByIdentifiers) {
        templateCellsByIdentifiers = @{}.mutableCopy;
        objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    UITableViewCell *templateCell = templateCellsByIdentifiers[identifier];
    
    if (!templateCell) {
        templateCell = [self dequeueReusableCellWithIdentifier:identifier];
        NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier);
        templateCell.fd_isTemplateLayoutCell = YES;
        templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO;
        templateCellsByIdentifiers[identifier] = templateCell;
        [self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]];
    }
    
    return templateCell;
}

  • 將所有創(chuàng)建的templateCell放置在一個(gè)字典templateCellsByIdentifiers中,并通過runtime將其加入內(nèi)存中作為屬性,實(shí)際上,templateCell 也是通過identifier在復(fù)用隊(duì)列中獲取復(fù)用的。所以,cell在使用前,應(yīng)先注冊(cè)為cell的復(fù)用對(duì)象。
  • 最后調(diào)用的[self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell]進(jìn)行高度計(jì)算。當(dāng)然也是最關(guān)鍵的一個(gè)操作,既然這是一個(gè)高度計(jì)算的框架,那么計(jì)算的步驟當(dāng)然是重中之重。
- (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell {
    CGFloat contentViewWidth = CGRectGetWidth(self.frame);
    
    if (cell.accessoryView) {
        //如果有定制accessoryView,則去除這個(gè)寬度
        contentViewWidth -= 16 + CGRectGetWidth(cell.accessoryView.frame);
    } else {
        //如果有系統(tǒng)accessoryView展示,則去除對(duì)應(yīng)的寬度。
        static const CGFloat systemAccessoryWidths[] = {
            [UITableViewCellAccessoryNone] = 0,
            [UITableViewCellAccessoryDisclosureIndicator] = 34,
            [UITableViewCellAccessoryDetailDisclosureButton] = 68,
            [UITableViewCellAccessoryCheckmark] = 40,
            [UITableViewCellAccessoryDetailButton] = 48
        };
        contentViewWidth -= systemAccessoryWidths[cell.accessoryType];
    }
    
    CGFloat fittingHeight = 0;
    
    if (!cell.fd_enforceFrameLayout && contentViewWidth > 0) {
        //如果是自動(dòng)布局,則將contentView的寬度約束添加進(jìn)去。
        //這樣做的目的是讓UILabel之類的內(nèi)容可能多行的控件適應(yīng)這個(gè)寬度折行(當(dāng)然前提是我們已經(jīng)設(shè)置好了這些控件的布局約束)。然后調(diào)用systemLayoutSizeFittingSize來計(jì)算高度。
        //最后移除剛才臨時(shí)添加的contentView寬度約束。
        NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth];
        [cell.contentView addConstraint:widthFenceConstraint];
        
        fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        [cell.contentView removeConstraint:widthFenceConstraint];
    }
    
    if (fittingHeight == 0) {
        // 嘗試調(diào)用 '- sizeThatFits:'進(jìn)行高度計(jì)算.
        // 注意:配件高度不應(yīng)包括分隔線視圖高度。
        fittingHeight = [cell sizeThatFits:CGSizeMake(contentViewWidth, 0)].height;
    }
    
    // 進(jìn)行完前面的邏輯后高度如果仍然為0,則使用默認(rèn)行高44
    if (fittingHeight == 0) {
        fittingHeight = 44;        
    }
    
    // 添加一像素作為tableView分割線高度。
    if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
        fittingHeight += 1.0 / [UIScreen mainScreen].scale;
    }
    
    return fittingHeight;
}

至此,就大致將這個(gè)框架分析的差不多了,源碼中,對(duì)類的實(shí)例化均為采用runtime添加AssociatedObject的方式。就不做解釋了。


最后

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

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

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