Skip to content

Commit 8724a06

Browse files
committed
Add template layout header footer view
1 parent f0e37b1 commit 8724a06

File tree

6 files changed

+140
-73
lines changed

6 files changed

+140
-73
lines changed

Diff for: Classes/UITableView+FDIndexPathHeightCache.m

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ - (FDIndexPathHeightCache *)fd_indexPathHeightCache {
133133
// We just forward primary call, in crash report, top most method in stack maybe FD's,
134134
// but it's really not our bug, you should check whether your table view's data source and
135135
// displaying cells are not matched when reloading.
136-
static void __FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(void(^callout)(void)) {
136+
static void __FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(void (^callout)(void)) {
137137
callout();
138138
}
139139
#define FDPrimaryCall(...) do {__FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(^{__VA_ARGS__});} while(0)

Diff for: Classes/UITableView+FDTemplateLayoutCell.h

+12
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,18 @@
7272

7373
@end
7474

75+
@interface UITableView (FDTemplateLayoutHeaderFooterView)
76+
77+
/// Returns header or footer view's height that registered in table view with reuse identifier.
78+
///
79+
/// Use it after calling "-[UITableView registerNib/Class:forHeaderFooterViewReuseIdentifier]",
80+
/// same with "-fd_heightForCellWithIdentifier:configuration:", it will call "-sizeThatFits:" for
81+
/// subclass of UITableViewHeaderFooterView which is not using Auto Layout.
82+
///
83+
- (CGFloat)fd_heightForHeaderFooterViewWithIdentifier:(NSString *)identifier configuration:(void (^)(id headerFooterView))configuration;
84+
85+
@end
86+
7587
@interface UITableViewCell (FDTemplateLayoutCell)
7688

7789
/// Indicate this is a template layout cell for calculation only.

Diff for: Classes/UITableView+FDTemplateLayoutCell.m

+117-56
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,82 @@
2525

2626
@implementation UITableView (FDTemplateLayoutCell)
2727

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+
28100
- (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier {
29101
NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
30102

31-
NSMutableDictionary<NSString *, __kindof UITableViewCell *> *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
103+
NSMutableDictionary<NSString *, UITableViewCell *> *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
32104
if (!templateCellsByIdentifiers) {
33105
templateCellsByIdentifiers = @{}.mutableCopy;
34106
objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
@@ -63,66 +135,14 @@ - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(
63135
configuration(templateLayoutCell);
64136
}
65137

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];
119139
}
120140

121141
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration {
122142
if (!identifier || !indexPath) {
123143
return 0;
124144
}
125-
145+
126146
// Hit cache
127147
if ([self.fd_indexPathHeightCache existsHeightAtIndexPath:indexPath]) {
128148
[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<
151171
CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
152172
[self.fd_keyedHeightCache cacheHeight:height byKey:key];
153173
[self fd_debugLog:[NSString stringWithFormat:@"cached by key[%@] - %@", key, @(height)]];
154-
174+
155175
return height;
156176
}
157177

158178
@end
159179

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+
160221
@implementation UITableViewCell (FDTemplateLayoutCell)
161222

162223
- (BOOL)fd_isTemplateLayoutCell {

Diff for: Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)