123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- // 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 <objc/runtime.h>
- @implementation UITableView (FDTemplateLayoutCell)
- - (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell {
- CGFloat contentViewWidth = CGRectGetWidth(self.frame);
-
- CGRect cellBounds = cell.bounds;
- cellBounds.size.width = contentViewWidth;
- cell.bounds = cellBounds;
-
- CGFloat rightSystemViewsWidth = 0.0;
- for (UIView *view in self.subviews) {
- if ([view isKindOfClass:NSClassFromString(@"UITableViewIndex")]) {
- rightSystemViewsWidth = CGRectGetWidth(view.frame);
- break;
- }
- }
-
- // 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) {
- rightSystemViewsWidth += 16 + CGRectGetWidth(cell.accessoryView.frame);
- } else {
- static const CGFloat systemAccessoryWidths[] = {
- [UITableViewCellAccessoryNone] = 0,
- [UITableViewCellAccessoryDisclosureIndicator] = 34,
- [UITableViewCellAccessoryDetailDisclosureButton] = 68,
- [UITableViewCellAccessoryCheckmark] = 40,
- [UITableViewCellAccessoryDetailButton] = 48
- };
- rightSystemViewsWidth += systemAccessoryWidths[cell.accessoryType];
- }
-
- if ([UIScreen mainScreen].scale >= 3 && [UIScreen mainScreen].bounds.size.width >= 414) {
- rightSystemViewsWidth += 4;
- }
-
- contentViewWidth -= rightSystemViewsWidth;
-
- // 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<NSLayoutConstraint *> *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:-rightSystemViewsWidth];
- 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<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;
- }
- - (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<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];
- [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<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 {
- 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
|