UITableView+FDTemplateLayoutCell.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. // The MIT License (MIT)
  2. //
  3. // Copyright (c) 2015-2016 forkingdog ( https://github.com/forkingdog )
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in all
  13. // copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. // SOFTWARE.
  22. #import "UITableView+FDTemplateLayoutCell.h"
  23. #import <objc/runtime.h>
  24. @implementation UITableView (FDTemplateLayoutCell)
  25. - (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell {
  26. CGFloat contentViewWidth = CGRectGetWidth(self.frame);
  27. // If a cell has accessory view or system accessory type, its content view's width is smaller
  28. // than cell's by some fixed values.
  29. if (cell.accessoryView) {
  30. contentViewWidth -= 16 + CGRectGetWidth(cell.accessoryView.frame);
  31. } else {
  32. static const CGFloat systemAccessoryWidths[] = {
  33. [UITableViewCellAccessoryNone] = 0,
  34. [UITableViewCellAccessoryDisclosureIndicator] = 34,
  35. [UITableViewCellAccessoryDetailDisclosureButton] = 68,
  36. [UITableViewCellAccessoryCheckmark] = 40,
  37. [UITableViewCellAccessoryDetailButton] = 48
  38. };
  39. contentViewWidth -= systemAccessoryWidths[cell.accessoryType];
  40. }
  41. // If not using auto layout, you have to override "-sizeThatFits:" to provide a fitting size by yourself.
  42. // This is the same height calculation passes used in iOS8 self-sizing cell's implementation.
  43. //
  44. // 1. Try "- systemLayoutSizeFittingSize:" first. (skip this step if 'fd_enforceFrameLayout' set to YES.)
  45. // 2. Warning once if step 1 still returns 0 when using AutoLayout
  46. // 3. Try "- sizeThatFits:" if step 1 returns 0
  47. // 4. Use a valid height or default row height (44) if not exist one
  48. CGFloat fittingHeight = 0;
  49. if (!cell.fd_enforceFrameLayout && contentViewWidth > 0) {
  50. // Add a hard width constraint to make dynamic content views (like labels) expand vertically instead
  51. // of growing horizontally, in a flow-layout manner.
  52. NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth];
  53. [cell.contentView addConstraint:widthFenceConstraint];
  54. // Auto layout engine does its math
  55. fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
  56. [cell.contentView removeConstraint:widthFenceConstraint];
  57. [self fd_debugLog:[NSString stringWithFormat:@"calculate using system fitting size (AutoLayout) - %@", @(fittingHeight)]];
  58. }
  59. if (fittingHeight == 0) {
  60. #if DEBUG
  61. // Warn if using AutoLayout but get zero height.
  62. if (cell.contentView.constraints.count > 0) {
  63. if (!objc_getAssociatedObject(self, _cmd)) {
  64. 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.");
  65. objc_setAssociatedObject(self, _cmd, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  66. }
  67. }
  68. #endif
  69. // Try '- sizeThatFits:' for frame layout.
  70. // Note: fitting height should not include separator view.
  71. fittingHeight = [cell sizeThatFits:CGSizeMake(contentViewWidth, 0)].height;
  72. [self fd_debugLog:[NSString stringWithFormat:@"calculate using sizeThatFits - %@", @(fittingHeight)]];
  73. }
  74. // Still zero height after all above.
  75. if (fittingHeight == 0) {
  76. // Use default row height.
  77. fittingHeight = 44;
  78. }
  79. // Add 1px extra space for separator line if needed, simulating default UITableViewCell.
  80. if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
  81. fittingHeight += 1.0 / [UIScreen mainScreen].scale;
  82. }
  83. return fittingHeight;
  84. }
  85. - (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier {
  86. NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
  87. NSMutableDictionary<NSString *, UITableViewCell *> *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
  88. if (!templateCellsByIdentifiers) {
  89. templateCellsByIdentifiers = @{}.mutableCopy;
  90. objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  91. }
  92. UITableViewCell *templateCell = templateCellsByIdentifiers[identifier];
  93. if (!templateCell) {
  94. templateCell = [self dequeueReusableCellWithIdentifier:identifier];
  95. NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier);
  96. templateCell.fd_isTemplateLayoutCell = YES;
  97. templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO;
  98. templateCellsByIdentifiers[identifier] = templateCell;
  99. [self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]];
  100. }
  101. return templateCell;
  102. }
  103. - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration {
  104. if (!identifier) {
  105. return 0;
  106. }
  107. UITableViewCell *templateLayoutCell = [self fd_templateCellForReuseIdentifier:identifier];
  108. // Manually calls to ensure consistent behavior with actual cells. (that are displayed on screen)
  109. [templateLayoutCell prepareForReuse];
  110. // Customize and provide content for our template cell.
  111. if (configuration) {
  112. configuration(templateLayoutCell);
  113. }
  114. return [self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell];
  115. }
  116. - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration {
  117. if (!identifier || !indexPath) {
  118. return 0;
  119. }
  120. // Hit cache
  121. if ([self.fd_indexPathHeightCache existsHeightAtIndexPath:indexPath]) {
  122. [self fd_debugLog:[NSString stringWithFormat:@"hit cache by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @([self.fd_indexPathHeightCache heightForIndexPath:indexPath])]];
  123. return [self.fd_indexPathHeightCache heightForIndexPath:indexPath];
  124. }
  125. CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
  126. [self.fd_indexPathHeightCache cacheHeight:height byIndexPath:indexPath];
  127. [self fd_debugLog:[NSString stringWithFormat: @"cached by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @(height)]];
  128. return height;
  129. }
  130. - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id<NSCopying>)key configuration:(void (^)(id cell))configuration {
  131. if (!identifier || !key) {
  132. return 0;
  133. }
  134. // Hit cache
  135. if ([self.fd_keyedHeightCache existsHeightForKey:key]) {
  136. CGFloat cachedHeight = [self.fd_keyedHeightCache heightForKey:key];
  137. [self fd_debugLog:[NSString stringWithFormat:@"hit cache by key[%@] - %@", key, @(cachedHeight)]];
  138. return cachedHeight;
  139. }
  140. CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
  141. [self.fd_keyedHeightCache cacheHeight:height byKey:key];
  142. [self fd_debugLog:[NSString stringWithFormat:@"cached by key[%@] - %@", key, @(height)]];
  143. return height;
  144. }
  145. @end
  146. @implementation UITableView (FDTemplateLayoutHeaderFooterView)
  147. - (__kindof UITableViewHeaderFooterView *)fd_templateHeaderFooterViewForReuseIdentifier:(NSString *)identifier {
  148. NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
  149. NSMutableDictionary<NSString *, UITableViewHeaderFooterView *> *templateHeaderFooterViews = objc_getAssociatedObject(self, _cmd);
  150. if (!templateHeaderFooterViews) {
  151. templateHeaderFooterViews = @{}.mutableCopy;
  152. objc_setAssociatedObject(self, _cmd, templateHeaderFooterViews, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  153. }
  154. UITableViewHeaderFooterView *templateHeaderFooterView = templateHeaderFooterViews[identifier];
  155. if (!templateHeaderFooterView) {
  156. templateHeaderFooterView = [self dequeueReusableHeaderFooterViewWithIdentifier:identifier];
  157. NSAssert(templateHeaderFooterView != nil, @"HeaderFooterView must be registered to table view for identifier - %@", identifier);
  158. templateHeaderFooterView.contentView.translatesAutoresizingMaskIntoConstraints = NO;
  159. templateHeaderFooterViews[identifier] = templateHeaderFooterView;
  160. [self fd_debugLog:[NSString stringWithFormat:@"layout header footer view created - %@", identifier]];
  161. }
  162. return templateHeaderFooterView;
  163. }
  164. - (CGFloat)fd_heightForHeaderFooterViewWithIdentifier:(NSString *)identifier configuration:(void (^)(id))configuration {
  165. UITableViewHeaderFooterView *templateHeaderFooterView = [self fd_templateHeaderFooterViewForReuseIdentifier:identifier];
  166. NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:templateHeaderFooterView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:CGRectGetWidth(self.frame)];
  167. [templateHeaderFooterView addConstraint:widthFenceConstraint];
  168. CGFloat fittingHeight = [templateHeaderFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
  169. [templateHeaderFooterView removeConstraint:widthFenceConstraint];
  170. if (fittingHeight == 0) {
  171. fittingHeight = [templateHeaderFooterView sizeThatFits:CGSizeMake(CGRectGetWidth(self.frame), 0)].height;
  172. }
  173. return fittingHeight;
  174. }
  175. @end
  176. @implementation UITableViewCell (FDTemplateLayoutCell)
  177. - (BOOL)fd_isTemplateLayoutCell {
  178. return [objc_getAssociatedObject(self, _cmd) boolValue];
  179. }
  180. - (void)setFd_isTemplateLayoutCell:(BOOL)isTemplateLayoutCell {
  181. objc_setAssociatedObject(self, @selector(fd_isTemplateLayoutCell), @(isTemplateLayoutCell), OBJC_ASSOCIATION_RETAIN);
  182. }
  183. - (BOOL)fd_enforceFrameLayout {
  184. return [objc_getAssociatedObject(self, _cmd) boolValue];
  185. }
  186. - (void)setFd_enforceFrameLayout:(BOOL)enforceFrameLayout {
  187. objc_setAssociatedObject(self, @selector(fd_enforceFrameLayout), @(enforceFrameLayout), OBJC_ASSOCIATION_RETAIN);
  188. }
  189. @end