|
25 | 25 |
|
26 | 26 | @implementation UITableView (FDTemplateLayoutCell)
|
27 | 27 |
|
| 28 | +- (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell { |
| 29 | + CGFloat contentViewWidth = CGRectGetWidth(self.frame); |
| 30 | + |
| 31 | + // If a cell has accessory view or system accessory type, its content view's width is smaller |
| 32 | + // than cell's by some fixed values. |
| 33 | + if (cell.accessoryView) { |
| 34 | + contentViewWidth -= 16 + CGRectGetWidth(cell.accessoryView.frame); |
| 35 | + } else { |
| 36 | + static const CGFloat systemAccessoryWidths[] = { |
| 37 | + [UITableViewCellAccessoryNone] = 0, |
| 38 | + [UITableViewCellAccessoryDisclosureIndicator] = 34, |
| 39 | + [UITableViewCellAccessoryDetailDisclosureButton] = 68, |
| 40 | + [UITableViewCellAccessoryCheckmark] = 40, |
| 41 | + [UITableViewCellAccessoryDetailButton] = 48 |
| 42 | + }; |
| 43 | + contentViewWidth -= systemAccessoryWidths[cell.accessoryType]; |
| 44 | + } |
| 45 | + |
| 46 | + // If not using auto layout, you have to override "-sizeThatFits:" to provide a fitting size by yourself. |
| 47 | + // This is the same height calculation passes used in iOS8 self-sizing cell's implementation. |
| 48 | + // |
| 49 | + // 1. Try "- systemLayoutSizeFittingSize:" first. (skip this step if 'fd_enforceFrameLayout' set to YES.) |
| 50 | + // 2. Warning once if step 1 still returns 0 when using AutoLayout |
| 51 | + // 3. Try "- sizeThatFits:" if step 1 returns 0 |
| 52 | + // 4. Use a valid height or default row height (44) if not exist one |
| 53 | + |
| 54 | + CGFloat fittingHeight = 0; |
| 55 | + |
| 56 | + if (!cell.fd_enforceFrameLayout && contentViewWidth > 0) { |
| 57 | + // Add a hard width constraint to make dynamic content views (like labels) expand vertically instead |
| 58 | + // of growing horizontally, in a flow-layout manner. |
| 59 | + NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth]; |
| 60 | + [cell.contentView addConstraint:widthFenceConstraint]; |
| 61 | + |
| 62 | + // Auto layout engine does its math |
| 63 | + fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; |
| 64 | + [cell.contentView removeConstraint:widthFenceConstraint]; |
| 65 | + |
| 66 | + [self fd_debugLog:[NSString stringWithFormat:@"calculate using system fitting size (AutoLayout) - %@", @(fittingHeight)]]; |
| 67 | + } |
| 68 | + |
| 69 | + if (fittingHeight == 0) { |
| 70 | +#if DEBUG |
| 71 | + // Warn if using AutoLayout but get zero height. |
| 72 | + if (cell.contentView.constraints.count > 0) { |
| 73 | + if (!objc_getAssociatedObject(self, _cmd)) { |
| 74 | + 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."); |
| 75 | + objc_setAssociatedObject(self, _cmd, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); |
| 76 | + } |
| 77 | + } |
| 78 | +#endif |
| 79 | + // Try '- sizeThatFits:' for frame layout. |
| 80 | + // Note: fitting height should not include separator view. |
| 81 | + fittingHeight = [cell sizeThatFits:CGSizeMake(contentViewWidth, 0)].height; |
| 82 | + |
| 83 | + [self fd_debugLog:[NSString stringWithFormat:@"calculate using sizeThatFits - %@", @(fittingHeight)]]; |
| 84 | + } |
| 85 | + |
| 86 | + // Still zero height after all above. |
| 87 | + if (fittingHeight == 0) { |
| 88 | + // Use default row height. |
| 89 | + fittingHeight = 44; |
| 90 | + } |
| 91 | + |
| 92 | + // Add 1px extra space for separator line if needed, simulating default UITableViewCell. |
| 93 | + if (self.separatorStyle != UITableViewCellSeparatorStyleNone) { |
| 94 | + fittingHeight += 1.0 / [UIScreen mainScreen].scale; |
| 95 | + } |
| 96 | + |
| 97 | + return fittingHeight; |
| 98 | +} |
| 99 | + |
28 | 100 | - (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier {
|
29 | 101 | NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
|
30 | 102 |
|
31 |
| - NSMutableDictionary<NSString *, __kindof UITableViewCell *> *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd); |
| 103 | + NSMutableDictionary<NSString *, UITableViewCell *> *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd); |
32 | 104 | if (!templateCellsByIdentifiers) {
|
33 | 105 | templateCellsByIdentifiers = @{}.mutableCopy;
|
34 | 106 | objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
@@ -63,66 +135,14 @@ - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(
|
63 | 135 | configuration(templateLayoutCell);
|
64 | 136 | }
|
65 | 137 |
|
66 |
| - CGFloat contentViewWidth = CGRectGetWidth(self.frame); |
67 |
| - |
68 |
| - // If a cell has accessory view or system accessory type, its content view's width is smaller |
69 |
| - // than cell's by some fixed values. |
70 |
| - if (templateLayoutCell.accessoryView) { |
71 |
| - contentViewWidth -= 16 + CGRectGetWidth(templateLayoutCell.accessoryView.frame); |
72 |
| - } else { |
73 |
| - static const CGFloat systemAccessoryWidths[] = { |
74 |
| - [UITableViewCellAccessoryNone] = 0, |
75 |
| - [UITableViewCellAccessoryDisclosureIndicator] = 34, |
76 |
| - [UITableViewCellAccessoryDetailDisclosureButton] = 68, |
77 |
| - [UITableViewCellAccessoryCheckmark] = 40, |
78 |
| - [UITableViewCellAccessoryDetailButton] = 48 |
79 |
| - }; |
80 |
| - contentViewWidth -= systemAccessoryWidths[templateLayoutCell.accessoryType]; |
81 |
| - } |
82 |
| - |
83 |
| - CGSize fittingSize = CGSizeZero; |
84 |
| - |
85 |
| - if (templateLayoutCell.fd_enforceFrameLayout) { |
86 |
| - // If not using auto layout, you have to override "-sizeThatFits:" to provide a fitting size by yourself. |
87 |
| - // This is the same method used in iOS8 self-sizing cell's implementation. |
88 |
| - // Note: fitting height should not include separator view. |
89 |
| - SEL selector = @selector(sizeThatFits:); |
90 |
| - BOOL inherited = ![templateLayoutCell isMemberOfClass:UITableViewCell.class]; |
91 |
| - BOOL overrided = [templateLayoutCell.class instanceMethodForSelector:selector] != [UITableViewCell instanceMethodForSelector:selector]; |
92 |
| - if (inherited && !overrided) { |
93 |
| - NSAssert(NO, @"Customized cell must override '-sizeThatFits:' method if not using auto layout."); |
94 |
| - } |
95 |
| - fittingSize = [templateLayoutCell sizeThatFits:CGSizeMake(contentViewWidth, 0)]; |
96 |
| - } else { |
97 |
| - // Add a hard width constraint to make dynamic content views (like labels) expand vertically instead |
98 |
| - // of growing horizontally, in a flow-layout manner. |
99 |
| - if (contentViewWidth > 0) { |
100 |
| - NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:templateLayoutCell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth]; |
101 |
| - [templateLayoutCell.contentView addConstraint:widthFenceConstraint]; |
102 |
| - // Auto layout engine does its math |
103 |
| - fittingSize = [templateLayoutCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; |
104 |
| - [templateLayoutCell.contentView removeConstraint:widthFenceConstraint]; |
105 |
| - } |
106 |
| - } |
107 |
| - |
108 |
| - // Add separator's height, using a private property in UITableViewCell. |
109 |
| - CGFloat separatorHeight = [[templateLayoutCell valueForKey:@"_separatorHeight"] doubleValue]; |
110 |
| - fittingSize.height += separatorHeight; |
111 |
| - |
112 |
| - if (templateLayoutCell.fd_enforceFrameLayout) { |
113 |
| - [self fd_debugLog:[NSString stringWithFormat:@"calculate using frame layout - %@", @(fittingSize.height)]]; |
114 |
| - } else { |
115 |
| - [self fd_debugLog:[NSString stringWithFormat:@"calculate using auto layout - %@", @(fittingSize.height)]]; |
116 |
| - } |
117 |
| - |
118 |
| - return fittingSize.height; |
| 138 | + return [self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell]; |
119 | 139 | }
|
120 | 140 |
|
121 | 141 | - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration {
|
122 | 142 | if (!identifier || !indexPath) {
|
123 | 143 | return 0;
|
124 | 144 | }
|
125 |
| - |
| 145 | + |
126 | 146 | // Hit cache
|
127 | 147 | if ([self.fd_indexPathHeightCache existsHeightAtIndexPath:indexPath]) {
|
128 | 148 | [self fd_debugLog:[NSString stringWithFormat:@"hit cache by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @([self.fd_indexPathHeightCache heightForIndexPath:indexPath])]];
|
@@ -151,12 +171,53 @@ - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id<
|
151 | 171 | CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
|
152 | 172 | [self.fd_keyedHeightCache cacheHeight:height byKey:key];
|
153 | 173 | [self fd_debugLog:[NSString stringWithFormat:@"cached by key[%@] - %@", key, @(height)]];
|
154 |
| - |
| 174 | + |
155 | 175 | return height;
|
156 | 176 | }
|
157 | 177 |
|
158 | 178 | @end
|
159 | 179 |
|
| 180 | +@implementation UITableView (FDTemplateLayoutHeaderFooterView) |
| 181 | + |
| 182 | +- (__kindof UITableViewHeaderFooterView *)fd_templateHeaderFooterViewForReuseIdentifier:(NSString *)identifier { |
| 183 | + NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier); |
| 184 | + |
| 185 | + NSMutableDictionary<NSString *, UITableViewHeaderFooterView *> *templateHeaderFooterViews = objc_getAssociatedObject(self, _cmd); |
| 186 | + if (!templateHeaderFooterViews) { |
| 187 | + templateHeaderFooterViews = @{}.mutableCopy; |
| 188 | + objc_setAssociatedObject(self, _cmd, templateHeaderFooterViews, OBJC_ASSOCIATION_RETAIN_NONATOMIC); |
| 189 | + } |
| 190 | + |
| 191 | + UITableViewHeaderFooterView *templateHeaderFooterView = templateHeaderFooterViews[identifier]; |
| 192 | + |
| 193 | + if (!templateHeaderFooterView) { |
| 194 | + templateHeaderFooterView = [self dequeueReusableHeaderFooterViewWithIdentifier:identifier]; |
| 195 | + NSAssert(templateHeaderFooterView != nil, @"HeaderFooterView must be registered to table view for identifier - %@", identifier); |
| 196 | + templateHeaderFooterView.contentView.translatesAutoresizingMaskIntoConstraints = NO; |
| 197 | + templateHeaderFooterViews[identifier] = templateHeaderFooterView; |
| 198 | + [self fd_debugLog:[NSString stringWithFormat:@"layout header footer view created - %@", identifier]]; |
| 199 | + } |
| 200 | + |
| 201 | + return templateHeaderFooterView; |
| 202 | +} |
| 203 | + |
| 204 | +- (CGFloat)fd_heightForHeaderFooterViewWithIdentifier:(NSString *)identifier configuration:(void (^)(id))configuration { |
| 205 | + UITableViewHeaderFooterView *templateHeaderFooterView = [self fd_templateHeaderFooterViewForReuseIdentifier:identifier]; |
| 206 | + |
| 207 | + NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:templateHeaderFooterView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:CGRectGetWidth(self.frame)]; |
| 208 | + [templateHeaderFooterView addConstraint:widthFenceConstraint]; |
| 209 | + CGFloat fittingHeight = [templateHeaderFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; |
| 210 | + [templateHeaderFooterView removeConstraint:widthFenceConstraint]; |
| 211 | + |
| 212 | + if (fittingHeight == 0) { |
| 213 | + fittingHeight = [templateHeaderFooterView sizeThatFits:CGSizeMake(CGRectGetWidth(self.frame), 0)].height; |
| 214 | + } |
| 215 | + |
| 216 | + return fittingHeight; |
| 217 | +} |
| 218 | + |
| 219 | +@end |
| 220 | + |
160 | 221 | @implementation UITableViewCell (FDTemplateLayoutCell)
|
161 | 222 |
|
162 | 223 | - (BOOL)fd_isTemplateLayoutCell {
|
|
0 commit comments