Initial commit
This commit is contained in:
+266
@@ -0,0 +1,266 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user