From 8724a0610fad0b60358d4dc2f16ec20a4a4bb49d Mon Sep 17 00:00:00 2001 From: Yuan Sun Date: Wed, 16 Mar 2016 11:35:34 +0800 Subject: [PATCH] Add template layout header footer view --- Classes/UITableView+FDIndexPathHeightCache.m | 2 +- Classes/UITableView+FDTemplateLayoutCell.h | 12 ++ Classes/UITableView+FDTemplateLayoutCell.m | 173 ++++++++++++------ .../contents.xcworkspacedata | 7 + Demo/Demo/Base.lproj/Main.storyboard | 17 +- Demo/Demo/FDFeedViewController.m | 2 +- 6 files changed, 140 insertions(+), 73 deletions(-) create mode 100644 Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Classes/UITableView+FDIndexPathHeightCache.m b/Classes/UITableView+FDIndexPathHeightCache.m index 47e2299..1fb77a1 100644 --- a/Classes/UITableView+FDIndexPathHeightCache.m +++ b/Classes/UITableView+FDIndexPathHeightCache.m @@ -133,7 +133,7 @@ - (FDIndexPathHeightCache *)fd_indexPathHeightCache { // We just forward primary call, in crash report, top most method in stack maybe FD's, // but it's really not our bug, you should check whether your table view's data source and // displaying cells are not matched when reloading. -static void __FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(void(^callout)(void)) { +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) diff --git a/Classes/UITableView+FDTemplateLayoutCell.h b/Classes/UITableView+FDTemplateLayoutCell.h index 9c7e957..ed628fc 100644 --- a/Classes/UITableView+FDTemplateLayoutCell.h +++ b/Classes/UITableView+FDTemplateLayoutCell.h @@ -72,6 +72,18 @@ @end +@interface UITableView (FDTemplateLayoutHeaderFooterView) + +/// Returns header or footer view's height that registered in table view with reuse identifier. +/// +/// Use it after calling "-[UITableView registerNib/Class:forHeaderFooterViewReuseIdentifier]", +/// same with "-fd_heightForCellWithIdentifier:configuration:", it will call "-sizeThatFits:" for +/// subclass of UITableViewHeaderFooterView which is not using Auto Layout. +/// +- (CGFloat)fd_heightForHeaderFooterViewWithIdentifier:(NSString *)identifier configuration:(void (^)(id headerFooterView))configuration; + +@end + @interface UITableViewCell (FDTemplateLayoutCell) /// Indicate this is a template layout cell for calculation only. diff --git a/Classes/UITableView+FDTemplateLayoutCell.m b/Classes/UITableView+FDTemplateLayoutCell.m index adcdf8f..ada68e2 100644 --- a/Classes/UITableView+FDTemplateLayoutCell.m +++ b/Classes/UITableView+FDTemplateLayoutCell.m @@ -25,10 +25,82 @@ @implementation UITableView (FDTemplateLayoutCell) +- (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell { + CGFloat contentViewWidth = CGRectGetWidth(self.frame); + + // 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) { + contentViewWidth -= 16 + CGRectGetWidth(cell.accessoryView.frame); + } else { + static const CGFloat systemAccessoryWidths[] = { + [UITableViewCellAccessoryNone] = 0, + [UITableViewCellAccessoryDisclosureIndicator] = 34, + [UITableViewCellAccessoryDetailDisclosureButton] = 68, + [UITableViewCellAccessoryCheckmark] = 40, + [UITableViewCellAccessoryDetailButton] = 48 + }; + contentViewWidth -= systemAccessoryWidths[cell.accessoryType]; + } + + // 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]; + [cell.contentView addConstraint:widthFenceConstraint]; + + // Auto layout engine does its math + fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; + [cell.contentView removeConstraint:widthFenceConstraint]; + + [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; +} + - (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier { NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier); - NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd); + NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd); if (!templateCellsByIdentifiers) { templateCellsByIdentifiers = @{}.mutableCopy; objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); @@ -63,66 +135,14 @@ - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:( configuration(templateLayoutCell); } - CGFloat contentViewWidth = CGRectGetWidth(self.frame); - - // 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 (templateLayoutCell.accessoryView) { - contentViewWidth -= 16 + CGRectGetWidth(templateLayoutCell.accessoryView.frame); - } else { - static const CGFloat systemAccessoryWidths[] = { - [UITableViewCellAccessoryNone] = 0, - [UITableViewCellAccessoryDisclosureIndicator] = 34, - [UITableViewCellAccessoryDetailDisclosureButton] = 68, - [UITableViewCellAccessoryCheckmark] = 40, - [UITableViewCellAccessoryDetailButton] = 48 - }; - contentViewWidth -= systemAccessoryWidths[templateLayoutCell.accessoryType]; - } - - CGSize fittingSize = CGSizeZero; - - if (templateLayoutCell.fd_enforceFrameLayout) { - // If not using auto layout, you have to override "-sizeThatFits:" to provide a fitting size by yourself. - // This is the same method used in iOS8 self-sizing cell's implementation. - // Note: fitting height should not include separator view. - SEL selector = @selector(sizeThatFits:); - BOOL inherited = ![templateLayoutCell isMemberOfClass:UITableViewCell.class]; - BOOL overrided = [templateLayoutCell.class instanceMethodForSelector:selector] != [UITableViewCell instanceMethodForSelector:selector]; - if (inherited && !overrided) { - NSAssert(NO, @"Customized cell must override '-sizeThatFits:' method if not using auto layout."); - } - fittingSize = [templateLayoutCell sizeThatFits:CGSizeMake(contentViewWidth, 0)]; - } else { - // Add a hard width constraint to make dynamic content views (like labels) expand vertically instead - // of growing horizontally, in a flow-layout manner. - if (contentViewWidth > 0) { - NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:templateLayoutCell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth]; - [templateLayoutCell.contentView addConstraint:widthFenceConstraint]; - // Auto layout engine does its math - fittingSize = [templateLayoutCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; - [templateLayoutCell.contentView removeConstraint:widthFenceConstraint]; - } - } - - // Add separator's height, using a private property in UITableViewCell. - CGFloat separatorHeight = [[templateLayoutCell valueForKey:@"_separatorHeight"] doubleValue]; - fittingSize.height += separatorHeight; - - if (templateLayoutCell.fd_enforceFrameLayout) { - [self fd_debugLog:[NSString stringWithFormat:@"calculate using frame layout - %@", @(fittingSize.height)]]; - } else { - [self fd_debugLog:[NSString stringWithFormat:@"calculate using auto layout - %@", @(fittingSize.height)]]; - } - - return fittingSize.height; + return [self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell]; } - (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])]]; @@ -151,12 +171,53 @@ - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id< 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; } @end +@implementation UITableView (FDTemplateLayoutHeaderFooterView) + +- (__kindof UITableViewHeaderFooterView *)fd_templateHeaderFooterViewForReuseIdentifier:(NSString *)identifier { + NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier); + + NSMutableDictionary *templateHeaderFooterViews = objc_getAssociatedObject(self, _cmd); + if (!templateHeaderFooterViews) { + templateHeaderFooterViews = @{}.mutableCopy; + objc_setAssociatedObject(self, _cmd, templateHeaderFooterViews, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + + UITableViewHeaderFooterView *templateHeaderFooterView = templateHeaderFooterViews[identifier]; + + if (!templateHeaderFooterView) { + templateHeaderFooterView = [self dequeueReusableHeaderFooterViewWithIdentifier:identifier]; + NSAssert(templateHeaderFooterView != nil, @"HeaderFooterView must be registered to table view for identifier - %@", identifier); + templateHeaderFooterView.contentView.translatesAutoresizingMaskIntoConstraints = NO; + templateHeaderFooterViews[identifier] = templateHeaderFooterView; + [self fd_debugLog:[NSString stringWithFormat:@"layout header footer view created - %@", identifier]]; + } + + return templateHeaderFooterView; +} + +- (CGFloat)fd_heightForHeaderFooterViewWithIdentifier:(NSString *)identifier configuration:(void (^)(id))configuration { + UITableViewHeaderFooterView *templateHeaderFooterView = [self fd_templateHeaderFooterViewForReuseIdentifier:identifier]; + + NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:templateHeaderFooterView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:CGRectGetWidth(self.frame)]; + [templateHeaderFooterView addConstraint:widthFenceConstraint]; + CGFloat fittingHeight = [templateHeaderFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; + [templateHeaderFooterView removeConstraint:widthFenceConstraint]; + + if (fittingHeight == 0) { + fittingHeight = [templateHeaderFooterView sizeThatFits:CGSizeMake(CGRectGetWidth(self.frame), 0)].height; + } + + return fittingHeight; +} + +@end + @implementation UITableViewCell (FDTemplateLayoutCell) - (BOOL)fd_isTemplateLayoutCell { diff --git a/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Demo/Demo/Base.lproj/Main.storyboard b/Demo/Demo/Base.lproj/Main.storyboard index 704d427..58f4094 100644 --- a/Demo/Demo/Base.lproj/Main.storyboard +++ b/Demo/Demo/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -12,7 +12,6 @@ - @@ -20,7 +19,6 @@ - @@ -29,7 +27,6 @@ - @@ -38,7 +35,6 @@ - @@ -51,38 +47,32 @@ - - @@ -100,7 +90,6 @@ - @@ -125,7 +114,6 @@ - @@ -146,7 +134,6 @@ - diff --git a/Demo/Demo/FDFeedViewController.m b/Demo/Demo/FDFeedViewController.m index eb950f3..2b48c51 100644 --- a/Demo/Demo/FDFeedViewController.m +++ b/Demo/Demo/FDFeedViewController.m @@ -75,7 +75,7 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - FDFeedCell *cell = [tableView dequeueReusableCellWithIdentifier:@"FDFeedCell" forIndexPath:indexPath]; + FDFeedCell *cell = [tableView dequeueReusableCellWithIdentifier:@"FDFeedCell"]; [self configureCell:cell atIndexPath:indexPath]; return cell; }