267 lines
13 KiB
Objective-C
267 lines
13 KiB
Objective-C
// 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);
|
|
|
|
// 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<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: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<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
|