// 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) - (id)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 *cell = [self fd_templateCellForReuseIdentifier:identifier]; // Manually calls to ensure consistent behavior with actual cells (that are displayed on screen). [cell prepareForReuse]; // Customize and provide content for our template cell. if (configuration) { configuration(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]; } CGSize fittingSize = CGSizeZero; if (cell.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 = ![cell isMemberOfClass:UITableViewCell.class]; BOOL overrided = [cell.class instanceMethodForSelector:selector] != [UITableViewCell instanceMethodForSelector:selector]; if (inherited && !overrided) { NSAssert(NO, @"Customized cell must override '-sizeThatFits:' method if not using auto layout."); } fittingSize = [cell 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. NSLayoutConstraint *tempWidthConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth]; [cell.contentView addConstraint:tempWidthConstraint]; // Auto layout engine does its math fittingSize = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; [cell.contentView removeConstraint:tempWidthConstraint]; } // Add 1px extra space for separator line if needed, simulating default UITableViewCell. if (self.separatorStyle != UITableViewCellSeparatorStyleNone) { fittingSize.height += 1.0 / [UIScreen mainScreen].scale; } if (cell.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; } - (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 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