16

I have created a very simple test case to reproduce this issue.

I am trying to set a footer view programmatically to a tableview. Please note that I am referring to the footer at the very bottom of the tableview - NOT the section footer (most stack overflow answers confuse them).

Here's my very simple code:

- (void)viewDidLoad {
    [super viewDidLoad];
    UIView *footerContainer = [[UIView alloc] initWithFrame:CGRectZero];
    footerContainer.backgroundColor=[UIColor greenColor];
    footerContainer.translatesAutoresizingMaskIntoConstraints=NO;
    [footerContainer addConstraints:@[[NSLayoutConstraint
                                       constraintWithItem:footerContainer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual
                                       toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:100
                                       ],
                                      [NSLayoutConstraint
                                       constraintWithItem:footerContainer attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual
                                       toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:[UIScreen mainScreen].bounds.size.width
                                       ]]];

    self.mytableview.tableFooterView=footerContainer;
    [self.view setNeedsLayout];
    [self.view layoutIfNeeded];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return 10;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    cell.textLabel.text=[NSString stringWithFormat:@"%ld",indexPath.row];
    return cell;
}

However, the outcome looks like this:

enter image description here

As you notice, the footer shows on top of the tableview. Is this a bug or am I missing something?

If I change the tableFooterView to tableHeaderView, then it works fine. So I was expecting the same to work for footer too but it doesn't.

  • 3
    Setting `translatesAutoresizingMaskIntoConstraints = NO` is likely breaking whatever method the tableview uses to position the footer. – dan Sep 25 '17 at 18:30
  • Are you using constraints because you will (or may) have dynamic content in the footer view? If so, I can give you a solution for that... – DonMag Sep 25 '17 at 19:34
  • @DonMag yes I was looking for a solution which will use constraints to set it up instead of setting the frame manually. My actual app has dynamic sized content for the footer. – sudoExclaimationExclaimation Sep 25 '17 at 20:20

6 Answers6

44

Do not set translatesAutoresizingMaskIntoConstraints = false on a UIView being assigned to tableView.tableFooterView.

In your case, this is footerContainer. I had this same problem. It was mentioned in a comment on the question, but I still spent hours troubleshooting before I noticed I was doing that, so I am putting it here as a possible answer as well.

gohnjanotis
  • 6,513
  • 6
  • 37
  • 57
6

I have tried the same with explicit frame values in swift and I achieved the behaviour as you asked for, Try it with the explicit frame value if you think its fine. And remove the layout constraints if its not needed.

   @IBOutlet weak var tableView: UITableView!

override func viewDidLoad() {
    super.viewDidLoad()

    tableView.delegate = self
    tableView.dataSource = self

    let footerView = UIView(frame: CGRect.init(x: 0, y: 0, width: tableView.frame.width, height: 50))
    footerView.backgroundColor = UIColor.green
    tableView.tableFooterView = footerView
}

extension ViewController: UITableViewDelegate, UITableViewDataSource {

func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 10
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
    cell?.textLabel?.text = "\(indexPath.row)"
    return cell!
}

}

enter image description here

Bharath
  • 2,064
  • 1
  • 14
  • 39
  • 1
    that does work for me yes. Does it mean auto layout is buggy with footer view? Should I log it as a bug with Apple? – sudoExclaimationExclaimation Sep 25 '17 at 18:29
  • @PranoyC : Happy it solved your issue, And sorry I don't have style of using manual auto layout code and can't help you with direct answer for your bug report idea, But I would suggest if you are about to raise an issue, raise after you try setting the auto layout for simple view in a view controller. – Bharath Sep 25 '17 at 18:35
  • This issue can be solved by using a container view. See my answer below. – Denis Kutlubaev Apr 02 '18 at 16:56
  • 1
    Your answer helped me by setting me in the right direction. I fixed it with using just the old friend autoresizing mask, the second I put even a constraint on my label, chaos unleashed. Definitely, an Apple bug right there. – Calin Drule Mar 17 '19 at 09:07
  • try a smaller device and you'll see how it looks. Wrong. – Matteo Gobbi Jan 25 '22 at 06:17
5

Dynamic-sized UITableView Header and Footer views do not always play nice with auto-layout, so you need to give it a little help.

Here is an example that creates a simple UIView for the footer view and adds an "expanding" UILabel (number of lines set to Zero). The footer view is created with an explicit CGRect for its frame, and the label is pinned to all four sides with auto-layout constraints.

In viewDidLayoutSubviews(), we tell auto-layout to calculate the frame of the footer view, based on the constraints on its contents, and then we update the frame values (well, specifically the height).

//
// this assumes IBOutlet has been set for "theTableView"
//

- (void)viewDidLoad {
    [super viewDidLoad];

    // standard stuff
    [_theTableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"simpleCell"];
    _theTableView.delegate = self;
    _theTableView.dataSource = self;

    // instantiate a view for the table footer
    // width doesn't matter (it will be stretched to fit the table by default)
    // set height to a big number to avoid a "will attempt to break constraint" warning
    UIView *footerContainer = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 1000)];

    // give it a color so we can see it
    footerContainer.backgroundColor=[UIColor greenColor];

    // set the footer view
    _theTableView.tableFooterView = footerContainer;


    // instantiate a label to add to the footer view
    UILabel *aLabel = [UILabel new];

    // auto-sizing the height, so set lines to zero
    aLabel.numberOfLines = 0;

    // give it a color so we can see it
    aLabel.backgroundColor = [UIColor yellowColor];

    // set the text to 8 lines for demonstration purposes
    aLabel.text = @"Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8";

    // standard, for auto-sizing
    aLabel.translatesAutoresizingMaskIntoConstraints = NO;

    // add the label to the footer view
    [footerContainer addSubview:aLabel];

    // constraint the label to 8-pts from each edge...
    [aLabel.topAnchor constraintEqualToAnchor:footerContainer.topAnchor constant:8.0].active = YES;
    [aLabel.leftAnchor constraintEqualToAnchor:footerContainer.leftAnchor constant:8.0].active = YES;
    [aLabel.rightAnchor constraintEqualToAnchor:footerContainer.rightAnchor constant:-8.0].active = YES;
    [aLabel.bottomAnchor constraintEqualToAnchor:footerContainer.bottomAnchor constant:-8.0].active = YES;

}

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    // get a reference to the table's footer view
    UIView *currentFooterView = [_theTableView tableFooterView];

    // if it's a valid reference (the table *does* have a footer view)
    if (currentFooterView) {

        // tell auto-layout to calculate the size based on the footer view's content
        CGFloat newHeight = [currentFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

        // get the current frame of the footer view
        CGRect currentFrame = currentFooterView.frame;

        // we only want to do this when necessary (otherwise we risk infinite recursion)
        // so... if the calculated height is not the same as the current height
        if (newHeight != currentFrame.size.height) {
            // use the new (calculated) height
            currentFrame.size.height = newHeight;
            currentFooterView.frame = currentFrame;
        }

    }

}

This can also be helpful when trying to get auto-sizing table view header views to work properly.

DonMag
  • 69,424
  • 5
  • 50
  • 86
1

The table view is ignoring the constraints.
Set the height manually and it'll work:

tableView.tableFooterView?.frame.size.height = 20
Yariv Nissim
  • 13,273
  • 1
  • 38
  • 44
0

You can use Autolayout and Xibs to create a footer view. But you have to put your custom view to the container view, that you assign to tableFooterView.

func setupTableFooterView() {
    // Create your footer view from XIB and set constraints (in my case it is historyToolBarView of class HistoryToolBarView)
    let view = Bundle.main.loadNibNamed("HistoryToolBarView", owner: self, options: nil)?.first
    historyToolBarView = view as! HistoryToolBarView
    historyToolBarView.translatesAutoresizingMaskIntoConstraints = false
    historyToolBarView.addConstraints(
        [NSLayoutConstraint.init(item: self.historyToolBarView,
                                 attribute: .height,
                                 relatedBy: .equal,
                                 toItem: nil,
                                 attribute: .notAnAttribute,
                                 multiplier: 1.0,
                                 constant: 60),
         NSLayoutConstraint.init(item: self.historyToolBarView,
                                 attribute: .width,
                                 relatedBy: .equal,
                                 toItem: nil,
                                 attribute: .notAnAttribute,
                                 multiplier: 1.0,
                                 constant: UIScreen.main.bounds.size.width)])

    // Create a container of your footer view called footerView and set it as a tableFooterView
    let footerView = UIView(frame: CGRect.init(x: 0, y: 0, width: tableView.frame.width, height: 60))
    footerView.backgroundColor = UIColor.green
    tableView.tableFooterView = footerView

    // Add your footer view to the container
    footerView.addSubview(historyToolBarView)
}
Denis Kutlubaev
  • 15,320
  • 6
  • 84
  • 70
0

set grouped style for table and set height for header CGFloat.leastNonzeroMagnitude

tableView.style = .grouped

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return CGFloat.leastNonzeroMagnitude
}
Marwan Alqadi
  • 795
  • 8
  • 14