// The MIT License (MIT) // // Copyright (c) 2015-2016 forkingdog ( https://github.com/forkingdog ) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #import "UITableView+FDTemplateLayoutCell.h" #import @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]; // [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 *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:0]; 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; } - (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier { NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier); NSMutableDictionary *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; } - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration { if (!identifier) { return 0; } UITableViewCell *templateLayoutCell = [self fd_templateCellForReuseIdentifier:identifier]; // 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); } 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])]]; 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)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; } @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 { return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setFd_isTemplateLayoutCell:(BOOL)isTemplateLayoutCell { objc_setAssociatedObject(self, @selector(fd_isTemplateLayoutCell), @(isTemplateLayoutCell), OBJC_ASSOCIATION_RETAIN); } - (BOOL)fd_enforceFrameLayout { return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setFd_enforceFrameLayout:(BOOL)enforceFrameLayout { objc_setAssociatedObject(self, @selector(fd_enforceFrameLayout), @(enforceFrameLayout), OBJC_ASSOCIATION_RETAIN); } @end