问题
I am trying to incorporate auto layout into my UITableViewHeaderFooterView subclass. The class is pretty basic, just two labels. This is the complete subclass:
@implementation MBTableDetailStyleFooterView
static void MBTableDetailStyleFooterViewCommonSetup(MBTableDetailStyleFooterView *_self) {
UILabel *rightLabel = [[UILabel alloc] init];
_self.rightLabel = rightLabel;
rightLabel.translatesAutoresizingMaskIntoConstraints = NO;
[_self.contentView addSubview:rightLabel];
UILabel *leftLabel = [[UILabel alloc] init];
_self.leftLabel = leftLabel;
leftLabel.translatesAutoresizingMaskIntoConstraints = NO;
[_self.contentView addSubview:leftLabel];
NSDictionary *views = NSDictionaryOfVariableBindings(rightLabel, leftLabel);
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-10-[leftLabel]-(>=10)-[rightLabel]-10-|" options:0 metrics:nil views:views];
[_self.contentView addConstraints:horizontalConstraints];
// center views vertically in super view
NSLayoutConstraint *leftCenterYConstraint = [NSLayoutConstraint constraintWithItem:leftLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_self.contentView attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
[_self.contentView addConstraint:leftCenterYConstraint];
NSLayoutConstraint *rightCenterYConstraint = [NSLayoutConstraint constraintWithItem:rightLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_self.contentView attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
[_self.contentView addConstraint:rightCenterYConstraint];
// same height for both labels
NSLayoutConstraint *sameHeightConstraint = [NSLayoutConstraint constraintWithItem:leftLabel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:rightLabel attribute:NSLayoutAttributeHeight multiplier:1 constant:0];
[_self.contentView addConstraint:sameHeightConstraint];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithReuseIdentifier:reuseIdentifier];
MBTableDetailStyleFooterViewCommonSetup(self);
return self;
}
@end
This class is used as a footer in the first section in a tableView with 2 sections. The first section contains dynamic items. The second section has only one row, which is used to add new items to the first section.
If there are no items in the first section I hide the footerView. So when I add the first new item I have to reload the section so the footerView appears. The code that does all this looks like this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if (indexPath.section == 1) {
BOOL sectionNeedsReload = ([self.data count] == 0); // reload section when no data (and therefor no footer) was present before the add
[self.data addObject:[NSDate date]];
NSIndexPath *newIndexPath = [NSIndexPath indexPathForRow:[self.data count]-1 inSection:0];
if (sectionNeedsReload) {
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
}
else {
[self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
[self configureFooter:(MBTableDetailStyleFooterView *)[tableView footerViewForSection:0] forSection:0];
}
}
- (void)configureFooter:(MBTableDetailStyleFooterView *)footer forSection:(NSInteger)section {
footer.leftLabel.text = @"Total";
footer.rightLabel.text = [NSString stringWithFormat:@"%d", [self.data count]];
}
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
MBTableDetailStyleFooterView *footer = nil;
if (section == 0 && [self.data count]) {
footer = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"Footer"];
[self configureFooter:footer forSection:section];
}
return footer;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
CGFloat height = 0;
if (section == 0 && [self.data count]) {
height = 20.0f;
}
return height;
}
Nothing really fancy. However, as soon as reloadSections:withRowAnimations:
is called on my tableView it throws an exception because it is "Unable to simultaneously satisfy constraints.".
Somewhere the tableView added a translated auto resizing mask constraint to my footer.
(
"<NSLayoutConstraint:0x718a1f0 H:[UILabel:0x7189130]-(10)-| (Names: '|':_UITableViewHeaderFooterContentView:0x7188df0 )>",
"<NSLayoutConstraint:0x7189e30 H:[UILabel:0x71892c0]-(>=10)-[UILabel:0x7189130]>",
"<NSLayoutConstraint:0x718a0a0 H:|-(10)-[UILabel:0x71892c0] (Names: '|':_UITableViewHeaderFooterContentView:0x7188df0 )>",
"<NSAutoresizingMaskLayoutConstraint:0x7591ab0 h=--& v=--& H:[_UITableViewHeaderFooterContentView:0x7188df0(0)]>"
)
When I replace reloadSections:withRowAnimations:
with a call to reloadData
no autoresizing mask constraint is added and everything works fine.
The interesting thing is that the exception tells me that it tries to break the constraint <NSLayoutConstraint:0x7189e30 H:[UILabel:0x71892c0]-(>=10)-[UILabel:0x7189130]>
But when I log the constraints in subsequent calls to configureFooter:forSection:
this constraint still exists, but the auto resizing mask constraint is gone
The constraints are exactly those that I have set up.
(
"<NSLayoutConstraint:0x718a0a0 H:|-(10)-[UILabel:0x71892c0] (Names: '|':_UITableViewHeaderFooterContentView:0x7188df0 )>",
"<NSLayoutConstraint:0x7189e30 H:[UILabel:0x71892c0]-(>=10)-[UILabel:0x7189130]>",
"<NSLayoutConstraint:0x718a1f0 H:[UILabel:0x7189130]-(10)-| (Names: '|':_UITableViewHeaderFooterContentView:0x7188df0 )>",
"<NSLayoutConstraint:0x718a3f0 UILabel:0x71892c0.centerY == _UITableViewHeaderFooterContentView:0x7188df0.centerY>",
"<NSLayoutConstraint:0x718a430 UILabel:0x7189130.centerY == _UITableViewHeaderFooterContentView:0x7188df0.centerY>",
"<NSLayoutConstraint:0x718a4b0 UILabel:0x71892c0.height == UILabel:0x7189130.height>"
)
Where does this auto resizing mask constraint come from? Where does it go?
Am I missing something? The first time I looked into auto layout was like a week ago, so this is totally possible.
回答1:
Working Solution as of iOS 9
In your UITableViewHeaderFooterView subclass place the following code.
- (void)setFrame:(CGRect)frame {
if (frame.size.width == 0) {
return;
}
[super setFrame:frame];
}
Explanation:
The tableview handles the layout of the header views and it does so by manually manipulating the frames (yes even with autolayout turned on).
If you inspect the width constraints that are on the header/footer views there are two, one contained on the superview (the table view) for the width, and one contained in the header/footer view itself for the width.
The constraint contained on the super view is a NSAutoresizingMaskLayoutConstraint which is the giveaway that the tableview depends on frames to manipulate the headers. Switching the translatesAutoresizingMaskIntoConstraints to NO on the header view affectively breaks its appearance which is another give away.
It appears that under some circumstances these header/footer views will have their frames change to a width of zero, for me it was when rows were inserted and the header views were reused. My guess is that somewhere in the UITableView code a preparation for an animation is made by starting the frame at zero width, even if you are not using an animation.
This solution should work well and should not impact scroll performance.
回答2:
I ran in to this last week.
The way that I've eliminated the warnings is to change my required constraints to have a priority of 999. This is a work around rather than a fix, but it does get around exceptions being thrown, caught and logged during layout.
Things that didn't work for me.
A suggestion is to set estimatedRowHeight. I tried to set the estimatedSectionHeaderHeight
, but this didn't help. Setting an estimatedSectionFooterHeight
created empty footers where I didn't want them, which was a bit odd.
I also tried setting translatesAutoresizingMaskIntoConstraints = NO; on the header footer view, and on its content view. Neither got rid of the warning and one lead to the layout breaking completely.
回答3:
I had a similar problem with just one extra label in the contentView. Try to insert
static void MBTableDetailStyleFooterViewCommonSetup(MBTableDetailStyleFooterView *_self) {
_self.contentView.translatesAutoresizingMaskIntoConstraints = NO
[...]
}
at first line in your MBTableDetailStyleFooterViewCommonSetup
function. For me, this works in conjunction with reloadSections:withRowAnimations:
.
Update:
I also added a new constraint for the contentView to use all width:
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|"
options:0
metrics:nil
views:@{@"contentView" : _self.contentView}];
[_self.contentView.superview addConstraints:horizontalConstraints];
回答4:
Annoyingly, it looks like UITableViewHeaderFooterView doesn't like constant constraints e.g.
|-10-[view]-10-|
I guess because at creation the view has zero width and cannot satisfy the constraint, which would need at least a 20px width in this case.
For me, the way around this was to change my constant constraints into something like
|-(<=10)-[view]-(<=10)-|
Which should satisfy a zero width content and should give the desired margin when resized.
回答5:
In iOS 11 I just could not get UITableViewHeaderFooterView
to display correctly.
The problem was that I was setting my constraints when the UITableViewHeaderFooterView
thought it's bounds were empty so it would always cause conflicts.
Here's the solution
override func layoutSubviews() {
super.layoutSubviews()
guard bounds != CGRect.zero else {
return
}
makeConstraints()
}
回答6:
So weird! Thanks for this @josh-bernfield, here is what I wrote for iOS 11.3:
override var frame: CGRect {
get {
return super.frame
}
set {
if newValue.width == 0 { return }
super.frame = newValue
}
}
回答7:
I don't see where you call translatesAutoresizingMaskIntoConstraints = NO on the footer itself. Should you do this when you create it?
- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithReuseIdentifier:reuseIdentifier];
self.translatesAutoresizingMaskIntoConstraints = NO;
MBTableDetailStyleFooterViewCommonSetup(self);
return self;
}
回答8:
I had a similar problem, only one UILabel in the contentView, and I used Masonry:
_titleLabel = ({
UILabel *label = [MLBUIFactory labelWithType:MALabelTypeB];
[self.contentView addSubview:label];
[label mas_makeConstraints:^(MASConstraintMaker */make) {
make.centerY.equalTo(self.contentView);
make.left.equalTo(self.contentView).offset(8);
make.right.equalTo(self.contentView).offset(-8);
}];
label;
});
then I got the warning:
(
"<MASLayoutConstraint:0x1742b2c60 UILabel:0x124557ee0.left == _UITableViewHeaderFooterContentView:0x12454c640.left + 8>",
"<MASLayoutConstraint:0x1742b3080 UILabel:0x124557ee0.right == _UITableViewHeaderFooterContentView:0x12454c640.right - 8>",
"<NSLayoutConstraint:0x174280780 _UITableViewHeaderFooterContentView:0x12454c640.width == 0>"
)
Will attempt to recover by breaking constraint
<MASLayoutConstraint:0x1742b3080 UILabel:0x124557ee0.right == _UITableViewHeaderFooterContentView:0x12454c640.right - 8>
so I tried to set self.translatesAutoresizingMaskIntoConstraints = NO;
, but it didn't works for me, so I focused on the warning message, and I'm confused, why the left constraint work but right, when I saw the _UITableViewHeaderFooterContentView:0x12454c640.width == 0
, I figured out, maybe that's why the right constraint is the breaking constraint, then I changed code:
_titleLabel = ({
UILabel *label = [MLBUIFactory labelWithType:MALabelTypeB];
[self.contentView addSubview:label];
[label mas_makeConstraints:^(MASConstraintMaker */make) {
make.centerY.equalTo(self.contentView);
make.left.equalTo(self.contentView).offset(8);
make.width.equalTo(@(SCREEN_WIDTH - 16));
}];
label;
});
I replaced the right constraint with width constraint, and the warning gone.
There has other way to let the warning gone: self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
, BUT the label's frame is wrong.
回答9:
Need to update contentView
frame
, and the warnings are gone. This maybe similar to Auto layout constraints issue on iOS7 in UITableViewCell
public class MyHeaderview: UITableViewHeaderFooterView {
// Add views to contentView
public override func layoutSubviews() {
super.layoutSubviews()
contentView.frame = bounds
}
}
回答10:
I think we can safely ignore the console warning.
I suspect the console log is caused by some intermediate state of the table view layout process.
When I add a footer section view with auto layout content, I get unsatisfiable constraints warning in the console as well.
From the log below, the constraint causing problem should be _UITableViewHeaderFooterContentView:0x7ff2115778d0.height == 0
2018-08-23 16:01:27.036159-0700 Attendee[45370:2760310] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x60000029d7e0 UILayoutGuide:0x6000003b98a0'UIViewLayoutMarginsGuide'.bottom == UIButton:0x7ff2115770a0.bottom + 20 (active)>",
"<NSLayoutConstraint:0x604000481d60 UILayoutGuide:0x6000003b98a0'UIViewLayoutMarginsGuide'.top == UIButton:0x7ff211576790'Forgot password?'.top (active)>",
"<NSLayoutConstraint:0x604000481c70 UIButton:0x7ff211576790'Forgot password?'.bottom == UIButton:0x7ff2115770a0.top - 8 (active)>",
"<NSLayoutConstraint:0x60000029d600 'UIView-bottomMargin-guide-constraint' V:[UILayoutGuide:0x6000003b98a0'UIViewLayoutMarginsGuide']-(8)-| (active, names: '|':_UITableViewHeaderFooterContentView:0x7ff2115778d0 )>",
"<NSLayoutConstraint:0x60000029da10 'UIView-Encapsulated-Layout-Height' _UITableViewHeaderFooterContentView:0x7ff2115778d0.height == 0 (active)>",
"<NSLayoutConstraint:0x60000029d560 'UIView-topMargin-guide-constraint' V:|-(8)-[UILayoutGuide:0x6000003b98a0'UIViewLayoutMarginsGuide'] (active, names: '|':_UITableViewHeaderFooterContentView:0x7ff2115778d0 )>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x60000029d7e0 UILayoutGuide:0x6000003b98a0'UIViewLayoutMarginsGuide'.bottom == UIButton:0x7ff2115770a0.bottom + 20 (active)>
If I override layoutSubviews
of the footer view to dump the constraints affecting vertical layout:
override func layoutSubviews() {
super.layoutSubviews()
print("after layout")
dump(contentView.constraintsAffectingLayout(for: .vertical))
}
I get the following output:
▿ 2 elements
- <NSLayoutConstraint:0x600000297110 'UIView-Encapsulated-Layout-Height' _UITableViewHeaderFooterContentView:0x7ffc22d88c20.height == 101 (active)> #0
- super: NSObject
- <NSAutoresizingMaskLayoutConstraint:0x6000002971b0 h=--& v=--& 'UIView-Encapsulated-Layout-Top' _UITableViewHeaderFooterContentView:0x7ffc22d88c20.minY == 0 (active, names: '|':Attendee.SearchFormButtonFooter:0x7ffc22d20da0 )> #1
- super: NSLayoutConstraint
- super: NSObject
There's no explicit layout constraint setting the height of the content view to 0. It's probably just the autosizing mask of content view causing the issue in some intermediate state of UITableView layout process.
回答11:
I came upon this issue and the best solution I found is to set a default frame in the init. That frame will be changed anyway but it is useful for the constraints that would break on a 0,0,0,0 frame.
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
frame = CGRect(x: 0, y: 0, width: 100, height: 100)
//... setup constraints...
}
来源:https://stackoverflow.com/questions/17581550/uitableviewheaderfooterview-subclass-with-auto-layout-and-section-reloading-won