UITableViewHeaderFooterView subclass with auto layout and section reloading won't work well together

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.

  • UITextView content size different in iOS7
  • Change the status bar background color color past iOS 7
  • How to SHA hash in cocoa/iOS
  • Calling Method From Another UIViewController Has No Visible Effect
  • Compass placing or position in a map view post iOS6
  • to run app continuously in the background
  • 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.

    7 Solutions Collect From Internet About “UITableViewHeaderFooterView subclass with auto layout and section reloading won't work well together”

    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.

    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.

    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];
    

    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;
    }
    

    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.

    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.

    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
        }
    }