Skip to content

Commit

Permalink
Merge pull request #185 from forkingdog/1.5-dev
Browse files Browse the repository at this point in the history
Add template layout header footer view
  • Loading branch information
sunnyxx committed Mar 16, 2016
2 parents 99d7682 + 66ea4a4 commit 2bead7b
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 75 deletions.
2 changes: 1 addition & 1 deletion Classes/UITableView+FDIndexPathHeightCache.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions Classes/UITableView+FDTemplateLayoutCell.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
176 changes: 118 additions & 58 deletions Classes/UITableView+FDTemplateLayoutCell.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSString *, __kindof UITableViewCell *> *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
NSMutableDictionary<NSString *, UITableViewCell *> *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
if (!templateCellsByIdentifiers) {
templateCellsByIdentifiers = @{}.mutableCopy;
objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
Expand All @@ -55,75 +127,22 @@ - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(

UITableViewCell *templateLayoutCell = [self fd_templateCellForReuseIdentifier:identifier];

// Manually calls to ensure consistent behavior with actual cells (that are displayed on screen).
// Manually calls to ensure consistent behavior with actual cells. (that are displayed on screen)
[templateLayoutCell prepareForReuse];

// Customize and provide content for our template cell.
if (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 1px extra space for separator line if needed, simulating default UITableViewCell.
if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
fittingSize.height += 1.0 / [UIScreen mainScreen].scale;
}

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])]];
Expand Down Expand Up @@ -152,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<NSString *, UITableViewHeaderFooterView *> *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 {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 2bead7b

Please sign in to comment.