PennyWise
PennyWise

Reputation: 647

How to keep tableFooterView always on bottom of UITableView

I have a UITableView with a variable amount of sections. Each section has a variable amount of cells and every section has a header and a footer. My UITableView also has a tableFooterView which I want to keep on the bottom of the screen at all times, except when the table is too large to fit on the screen, then the tableFooterView should be shown below the last section. What I want to accomplish is illustrated here:

Example of what I want, scenario 1

Example of what I want, scenario 2

However, currently the tableFooterView is always located right beneath the last section, so when there are for example only two sections, it looks like this:

Example of what I currently have

I am looking for a way to keep it always at the bottom, in every possible scenario. I have been looking around and because Apple doesn't support AutoLayout for the tableFooterView, I haven't found a solution yet. Similar cases replace the tableFooterView with a sectionFooter on the last section, but I can't do that as I already have sectionFooters.

Is there anybody who can help me out or point me towards the right direction? A couple of things to consider:

How I set up the tableFooterView at the moment:

class CustomView: UITableViewDelegate, UITableViewDataSource {

    var myTableFooter: UIView = {

        let myTableFooter = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
        myTableFooter.backgroundColor = .red
        myTableFooter.isUserInteractionEnabled = true
        return myTableFooter

    }()

    override init(frame: CGRect) {

        super.init(frame: frame)
        setupViews()

        MyTableView.tableFooterView = myTableFooter

    }

}

EDIT: Tried the scrollViewDidScroll method as suggested, but didn't work:

func scrollViewDidScroll(_ scrollView: UIScrollView) {

    if(scrollView == myTableView) {

        let neededHeight = myTableView.frame.height - 50 - view.safeAreaInsets.bottom
        let currentHeight = myTableView.contentSize.height - 50

        let heightDifference = neededHeight - currentHeight

        if(heightDifference > 0) {

            myTableView.tableFooterView?.transform = CGAffineTransform(translationX: 0, y: heightDifference)

        }

    }

}

Upvotes: 4

Views: 6043

Answers (3)

irons163
irons163

Reputation: 54

I made a demo in Github:StickTableFooterView

ScreenShot

How to keep tableFooterView always on bottom of UITableView?

  1. Creating an view as tableFooterView.
  2. Creating an view and put it into tableFooterView as a subview(Pretended tableFooterView).
  3. Setting proper layout constraints.
  4. Enjoy your tableFooterView ticking on bottom of UITableView.
  • Becuase the pretended tableFooterView is out of the bound of its superview, need to deal with the touch event. See ViewWithOutboundsButtons.m.

About demo

  • Red area refers the tableFooterView.
  • Yellow area refers the pretended tableFooterView.

-----Updated------

Setting proper layout constraints:

innerView.translatesAutoresizingMaskIntoConstraints = false;
[NSLayoutConstraint constraintWithItem:innerView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:innerView.superview attribute:NSLayoutAttributeRight multiplier:1 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:innerView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:innerView.superview attribute:NSLayoutAttributeLeft multiplier:1 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:innerView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:innerView.superview attribute:NSLayoutAttributeBottom multiplier:1 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:innerView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self.tableView.superview attribute:NSLayoutAttributeBottom multiplier:1 constant:0].active = YES;
[NSLayoutConstraint constraintWithItem:innerView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:innerViewHeight].active = YES;

The key point is set layout constraint with tableView.superview:

[NSLayoutConstraint constraintWithItem:innerView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self.tableView.superview attribute:NSLayoutAttributeBottom multiplier:1 constant:0].active = YES;

Upvotes: 2

DonMag
DonMag

Reputation: 77423

One approach would be:

  • use an extension to define a "self-sizing non-scrolling" table view
  • embed the table view and a normal UIView for the "footer" view in a "container" view
  • embed the container view in a scroll view, with a height equal to the scroll view but with a low priority
  • constrain the footer view to the bottom of the container view, and >= to the bottom of the table view

So, the "auto-height" of the tableView + the height of the footer view determines the height of the container view, which determines the .contentSize of the scroll view. The footer view will "stick" to the bottom of the container view. When the scroll view has enough content, it will "push down" the footer view.

Example:

enter image description here

enter image description here

Here is the code to create that. Everything is done via code... no IBOutlets needed, so just create a new view controller and assign its class to PennyWiseViewController:

//
//  PennyWiseViewController.swift
//
//  Created by Don Mag on 5/14/19.
//

import UIKit

final class ContentSizedTableView: UITableView {

    override var contentSize:CGSize {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }

    override var intrinsicContentSize: CGSize {
        layoutIfNeeded()
        return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
    }

}

class MyOneLabelCell: UITableViewCell {

    // very simple one-label tableView cell

    let theLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.numberOfLines = 0
        return v
    }()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        contentView.addSubview(theLabel)

        NSLayoutConstraint.activate([
            theLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8.0),
            theLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8.0),
            theLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0),
            theLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0),
            ])

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

class PennyWiseViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    let theContainerView: UIView = {
        let v = UIView()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    let theScrollView: UIScrollView = {
        let v = UIScrollView()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    let theTableView: ContentSizedTableView = {
        let v = ContentSizedTableView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.isScrollEnabled = false
        return v
    }()

    let theFooterView: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .red
        v.textColor = .white
        v.text = "The Footer View"
        v.textAlignment = .center
        return v
    }()

    // start with 3 sections
    // selecting the row in the first section allows adding sections
    // selecting the row in the second section allows deleting sections
    var numSections = 3

    let reuseID = "MyOneLabelCell"

    override func viewDidLoad() {
        super.viewDidLoad()

        theTableView.dataSource = self
        theTableView.delegate = self

        theTableView.register(MyOneLabelCell.self, forCellReuseIdentifier: reuseID)

        // add the views
        view.addSubview(theScrollView)
        theScrollView.addSubview(theContainerView)
        theContainerView.addSubview(theTableView)
        theContainerView.addSubview(theFooterView)

        // this will allow the container height to be at least the height of the scroll view
        // when enough content is added to the container, it will grow
        let containerHeightConstraint = theContainerView.heightAnchor.constraint(equalTo: theScrollView.heightAnchor, multiplier: 1.0)
        containerHeightConstraint.priority = .defaultLow

        NSLayoutConstraint.activate([

            // constrain scrollView to all 4 sides (safe-area)
            theScrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            theScrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            theScrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            theScrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),

            // constrain containerView to all 4 sides of scrollView
            theContainerView.topAnchor.constraint(equalTo: theScrollView.topAnchor),
            theContainerView.bottomAnchor.constraint(equalTo: theScrollView.bottomAnchor),
            theContainerView.leadingAnchor.constraint(equalTo: theScrollView.leadingAnchor),
            theContainerView.trailingAnchor.constraint(equalTo: theScrollView.trailingAnchor),

            theContainerView.widthAnchor.constraint(equalTo: theScrollView.widthAnchor),

            // constrain tableView to top/leading/trailing of constainerView
            theTableView.topAnchor.constraint(equalTo: theContainerView.topAnchor),
            theTableView.leadingAnchor.constraint(equalTo: theContainerView.leadingAnchor),
            theTableView.trailingAnchor.constraint(equalTo: theContainerView.trailingAnchor),

            // constrain footerView >= 20 from bottom of tableView
            theFooterView.topAnchor.constraint(greaterThanOrEqualTo: theTableView.bottomAnchor, constant: 20.0),

            theFooterView.leadingAnchor.constraint(equalTo: theContainerView.leadingAnchor, constant: 0.0),
            theFooterView.trailingAnchor.constraint(equalTo: theContainerView.trailingAnchor, constant: 0.0),
            theFooterView.bottomAnchor.constraint(equalTo: theContainerView.bottomAnchor, constant: 0.0),

            theFooterView.heightAnchor.constraint(equalToConstant: 150.0),

            containerHeightConstraint,

            ])

    }


    // MARK: - Table view data source

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

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return section < 2 ? 1 : 2
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: reuseID, for: indexPath) as! MyOneLabelCell

        switch indexPath.section {
        case 0:
            cell.theLabel.text = "Add a section"
        case 1:
            cell.theLabel.text = "Delete a section"
        default:
            cell.theLabel.text = "\(indexPath)"
        }

        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)

        switch indexPath.section {
        case 0:
            numSections += 1
            tableView.reloadData()
        case 1:
            if numSections > 2 {
                numSections -= 1
                tableView.reloadData()
            }
        default:
            print("\(indexPath) was selected")
        }

    }

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return "Section \(section) Header"
    }

    func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
        return "Section \(section) Footer"
    }

}

Upvotes: 2

Rengers
Rengers

Reputation: 15218

You might be able to do this by manually translating the frame of the footer view when you scroll the table. You will need to do the following:

  1. Set the view as a tableFooterView.
  2. Respond to the scrollViewDidScroll method of UIScrollViewDelegate.
  3. Calculate the amount to offset the footer view and set that as a transform: tableView.tableFooterView?.transform = CGAffineTransform(translationX: 0, y: <some value>)

Upvotes: 0

Related Questions