Reputation: 647
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:
tableFooterView
;UITableView
and rows to the sections, so the tableFooterView should then update its locationHow 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
Reputation: 54
I made a demo in Github:StickTableFooterView
-----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
Reputation: 77423
One approach would be:
UIView
for the "footer" view in a "container" view>=
to the bottom of the table viewSo, 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"
}
}
Upvotes: 2
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:
tableFooterView
.scrollViewDidScroll
method of UIScrollViewDelegate.tableView.tableFooterView?.transform = CGAffineTransform(translationX: 0, y: <some value>)
Upvotes: 0