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:


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