UITableView+FDTemplateLayoutCell.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  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. CGRect cellBounds = cell.bounds;
  28. cellBounds.size.width = contentViewWidth;
  29. cell.bounds = cellBounds;
  30. CGFloat rightSystemViewsWidth = 0.0;
  31. for (UIView *view in self.subviews) {
  32. if ([view isKindOfClass:NSClassFromString(@"UITableViewIndex")]) {
  33. rightSystemViewsWidth = CGRectGetWidth(view.frame);
  34. break;
  35. }
  36. }
  37. // If a cell has accessory view or system accessory type, its content view's width is smaller
  38. // than cell's by some fixed values.
  39. if (cell.accessoryView) {
  40. rightSystemViewsWidth += 16 + CGRectGetWidth(cell.accessoryView.frame);
  41. } else {
  42. static const CGFloat systemAccessoryWidths[] = {
  43. [UITableViewCellAccessoryNone] = 0,
  44. [UITableViewCellAccessoryDisclosureIndicator] = 34,
  45. [UITableViewCellAccessoryDetailDisclosureButton] = 68,
  46. [UITableViewCellAccessoryCheckmark] = 40,
  47. [UITableViewCellAccessoryDetailButton] = 48
  48. };
  49. rightSystemViewsWidth += systemAccessoryWidths[cell.accessoryType];
  50. }
  51. if ([UIScreen mainScreen].scale >= 3 && [UIScreen mainScreen].bounds.size.width >= 414) {
  52. rightSystemViewsWidth += 4;
  53. }
  54. contentViewWidth -= rightSystemViewsWidth;
  55. // If not using auto layout, you have to override "-sizeThatFits:" to provide a fitting size by yourself.
  56. // This is the same height calculation passes used in iOS8 self-sizing cell's implementation.
  57. //
  58. // 1. Try "- systemLayoutSizeFittingSize:" first. (skip this step if 'fd_enforceFrameLayout' set to YES.)
  59. // 2. Warning once if step 1 still returns 0 when using AutoLayout
  60. // 3. Try "- sizeThatFits:" if step 1 returns 0
  61. // 4. Use a valid height or default row height (44) if not exist one
  62. CGFloat fittingHeight = 0;
  63. if (!cell.fd_enforceFrameLayout && contentViewWidth > 0) {
  64. // Add a hard width constraint to make dynamic content views (like labels) expand vertically instead
  65. // of growing horizontally, in a flow-layout manner.
  66. NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth];
  67. // [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.
  68. static BOOL isSystemVersionEqualOrGreaterThen10_2 = NO;
  69. static dispatch_once_t onceToken;
  70. dispatch_once(&onceToken, ^{
  71. isSystemVersionEqualOrGreaterThen10_2 = [UIDevice.currentDevice.systemVersion compare:@"10.2" options:NSNumericSearch] != NSOrderedAscending;
  72. });
  73. NSArray<NSLayoutConstraint *> *edgeConstraints;
  74. if (isSystemVersionEqualOrGreaterThen10_2) {
  75. // To avoid confilicts, make width constraint softer than required (1000)
  76. widthFenceConstraint.priority = UILayoutPriorityRequired - 1;
  77. // Build edge constraints
  78. NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0];
  79. NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeRight multiplier:1.0 constant:-rightSystemViewsWidth];
  80. NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];
  81. NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0];
  82. edgeConstraints = @[leftConstraint, rightConstraint, topConstraint, bottomConstraint];
  83. [cell addConstraints:edgeConstraints];
  84. }
  85. [cell.contentView addConstraint:widthFenceConstraint];
  86. // Auto layout engine does its math
  87. fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
  88. // Clean-ups
  89. [cell.contentView removeConstraint:widthFenceConstraint];
  90. if (isSystemVersionEqualOrGreaterThen10_2) {
  91. [cell removeConstraints:edgeConstraints];
  92. }
  93. [self fd_debugLog:[NSString stringWithFormat:@"calculate using system fitting size (AutoLayout) - %@", @(fittingHeight)]];
  94. }
  95. if (fittingHeight == 0) {
  96. #if DEBUG
  97. // Warn if using AutoLayout but get zero height.
  98. if (cell.contentView.constraints.count > 0) {
  99. if (!objc_getAssociatedObject(self, _cmd)) {
  100. 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.");
  101. objc_setAssociatedObject(self, _cmd, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  102. }
  103. }
  104. #endif
  105. // Try '- sizeThatFits:' for frame layout.
  106. // Note: fitting height should not include separator view.
  107. fittingHeight = [cell sizeThatFits:CGSizeMake(contentViewWidth, 0)].height;
  108. [self fd_debugLog:[NSString stringWithFormat:@"calculate using sizeThatFits - %@", @(fittingHeight)]];
  109. }
  110. // Still zero height after all above.
  111. if (fittingHeight == 0) {
  112. // Use default row height.
  113. fittingHeight = 44;
  114. }
  115. // Add 1px extra space for separator line if needed, simulating default UITableViewCell.
  116. if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
  117. fittingHeight += 1.0 / [UIScreen mainScreen].scale;
  118. }
  119. return fittingHeight;
  120. }
  121. - (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier {
  122. NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
  123. NSMutableDictionary<NSString *, UITableViewCell *> *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
  124. if (!templateCellsByIdentifiers) {
  125. templateCellsByIdentifiers = @{}.mutableCopy;
  126. objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  127. }
  128. UITableViewCell *templateCell = templateCellsByIdentifiers[identifier];
  129. if (!templateCell) {
  130. templateCell = [self dequeueReusableCellWithIdentifier:identifier];
  131. NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier);
  132. templateCell.fd_isTemplateLayoutCell = YES;
  133. templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO;
  134. templateCellsByIdentifiers[identifier] = templateCell;
  135. [self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]];
  136. }
  137. return templateCell;
  138. }
  139. - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration {
  140. if (!identifier) {
  141. return 0;
  142. }
  143. UITableViewCell *templateLayoutCell = [self fd_templateCellForReuseIdentifier:identifier];
  144. // Manually calls to ensure consistent behavior with actual cells. (that are displayed on screen)
  145. [templateLayoutCell prepareForReuse];
  146. // Customize and provide content for our template cell.
  147. if (configuration) {
  148. configuration(templateLayoutCell);
  149. }
  150. return [self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell];
  151. }
  152. - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration {
  153. if (!identifier || !indexPath) {
  154. return 0;
  155. }
  156. // Hit cache
  157. if ([self.fd_indexPathHeightCache existsHeightAtIndexPath:indexPath]) {
  158. [self fd_debugLog:[NSString stringWithFormat:@"hit cache by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @([self.fd_indexPathHeightCache heightForIndexPath:indexPath])]];
  159. return [self.fd_indexPathHeightCache heightForIndexPath:indexPath];
  160. }
  161. CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
  162. [self.fd_indexPathHeightCache cacheHeight:height byIndexPath:indexPath];
  163. [self fd_debugLog:[NSString stringWithFormat: @"cached by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @(height)]];
  164. return height;
  165. }
  166. - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id<NSCopying>)key configuration:(void (^)(id cell))configuration {
  167. if (!identifier || !key) {
  168. return 0;
  169. }
  170. // Hit cache
  171. if ([self.fd_keyedHeightCache existsHeightForKey:key]) {
  172. CGFloat cachedHeight = [self.fd_keyedHeightCache heightForKey:key];
  173. [self fd_debugLog:[NSString stringWithFormat:@"hit cache by key[%@] - %@", key, @(cachedHeight)]];
  174. return cachedHeight;
  175. }
  176. CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
  177. [self.fd_keyedHeightCache cacheHeight:height byKey:key];
  178. [self fd_debugLog:[NSString stringWithFormat:@"cached by key[%@] - %@", key, @(height)]];
  179. return height;
  180. }
  181. @end
  182. @implementation UITableView (FDTemplateLayoutHeaderFooterView)
  183. - (__kindof UITableViewHeaderFooterView *)fd_templateHeaderFooterViewForReuseIdentifier:(NSString *)identifier {
  184. NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
  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. UITableViewHeaderFooterView *templateHeaderFooterView = templateHeaderFooterViews[identifier];
  191. if (!templateHeaderFooterView) {
  192. templateHeaderFooterView = [self dequeueReusableHeaderFooterViewWithIdentifier:identifier];
  193. NSAssert(templateHeaderFooterView != nil, @"HeaderFooterView must be registered to table view for identifier - %@", identifier);
  194. templateHeaderFooterView.contentView.translatesAutoresizingMaskIntoConstraints = NO;
  195. templateHeaderFooterViews[identifier] = templateHeaderFooterView;
  196. [self fd_debugLog:[NSString stringWithFormat:@"layout header footer view created - %@", identifier]];
  197. }
  198. return templateHeaderFooterView;
  199. }
  200. - (CGFloat)fd_heightForHeaderFooterViewWithIdentifier:(NSString *)identifier configuration:(void (^)(id))configuration {
  201. UITableViewHeaderFooterView *templateHeaderFooterView = [self fd_templateHeaderFooterViewForReuseIdentifier:identifier];
  202. NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:templateHeaderFooterView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:CGRectGetWidth(self.frame)];
  203. [templateHeaderFooterView addConstraint:widthFenceConstraint];
  204. CGFloat fittingHeight = [templateHeaderFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
  205. [templateHeaderFooterView removeConstraint:widthFenceConstraint];
  206. if (fittingHeight == 0) {
  207. fittingHeight = [templateHeaderFooterView sizeThatFits:CGSizeMake(CGRectGetWidth(self.frame), 0)].height;
  208. }
  209. return fittingHeight;
  210. }
  211. @end
  212. @implementation UITableViewCell (FDTemplateLayoutCell)
  213. - (BOOL)fd_isTemplateLayoutCell {
  214. return [objc_getAssociatedObject(self, _cmd) boolValue];
  215. }
  216. - (void)setFd_isTemplateLayoutCell:(BOOL)isTemplateLayoutCell {
  217. objc_setAssociatedObject(self, @selector(fd_isTemplateLayoutCell), @(isTemplateLayoutCell), OBJC_ASSOCIATION_RETAIN);
  218. }
  219. - (BOOL)fd_enforceFrameLayout {
  220. return [objc_getAssociatedObject(self, _cmd) boolValue];
  221. }
  222. - (void)setFd_enforceFrameLayout:(BOOL)enforceFrameLayout {
  223. objc_setAssociatedObject(self, @selector(fd_enforceFrameLayout), @(enforceFrameLayout), OBJC_ASSOCIATION_RETAIN);
  224. }
  225. @end