Skip to content

Instantly share code, notes, and snippets.

@alexmx
Created April 7, 2017 13:30
Show Gist options
  • Save alexmx/3bd217b25542fc3dd41fa79cfe2a22c7 to your computer and use it in GitHub Desktop.
Save alexmx/3bd217b25542fc3dd41fa79cfe2a22c7 to your computer and use it in GitHub Desktop.
Adjust UITableView footer view to fill the whole remaining part of the screen.
@mpvosseller
Copy link

mpvosseller commented Jan 4, 2018

This is great. FWIW I had to replace self.contentInset.top with self.adjustedContentInset.top to get it working for me.
I also tweaked it to allow Auto Layout to determine the minHeight. My swift version looks like this:

    func adjustFooterViewHeightToFillTableView() {
        
        // Invoke from UITableViewController.viewDidLayoutSubviews()
        
        if let tableFooterView = self.tableFooterView {
            
            let minHeight = tableFooterView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
            
            let currentFooterHeight = tableFooterView.frame.height
            
            let fitHeight = self.frame.height - self.adjustedContentInset.top - self.contentSize.height  + currentFooterHeight
            let nextHeight = (fitHeight > minHeight) ? fitHeight : minHeight
            
            if (round(nextHeight) != round(currentFooterHeight)) {
                var frame = tableFooterView.frame
                frame.size.height = nextHeight
                tableFooterView.frame = frame
                self.tableFooterView = tableFooterView
            }
        }
    }

@endy-s
Copy link

endy-s commented Feb 13, 2019

I don't know why I couldn't make the tableFooterView work, so I used the section footer instead.

I know it is a different approach, but it may help someone.
Have in mind that, for this approach to work, you need to have static sections and cell's height.

    // Add this to your UITableViewDataSource

    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        // I have a dictionary called sections  ([Int: [CellInfo]]) that contains all my TableView's infos
        if section == sections.count - 1 {
            let minHeight: CGFloat = 80 // TODO: Change for your own footer min height

            var rowsSize: CGFloat = 0
            var sectionSeparatorsSize: CGFloat = 0
            sections.keys.forEach({ (key) in
                sectionSeparatorsSize += (tableView.sectionFooterHeight + tableView.sectionHeaderHeight)
                rowsSize += tableView.rowHeight * CGFloat(sections[key]?.count ?? 0)
            })

            let fitHeight = tableView.frame.height - tableView.adjustedContentInset.top - sectionSeparatorsSize - rowsSize

            return (fitHeight > minHeight) ? fitHeight : minHeight
        }
        return tableView.sectionFooterHeight
    }

@Claes34
Copy link

Claes34 commented Nov 13, 2019

This is great. FWIW I had to replace self.contentInset.top with self.adjustedContentInset.top to get it working for me.
I also tweaked it to allow Auto Layout to determine the minHeight. My swift version looks like this:

    func adjustFooterViewHeightToFillTableView() {
        
        // Invoke from UITableViewController.viewDidLayoutSubviews()
        
        if let tableFooterView = self.tableFooterView {
            
            let minHeight = tableFooterView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
            
            let currentFooterHeight = tableFooterView.frame.height
            
            let fitHeight = self.frame.height - self.adjustedContentInset.top - self.contentSize.height  + currentFooterHeight
            let nextHeight = (fitHeight > minHeight) ? fitHeight : minHeight
            
            if (round(nextHeight) != round(currentFooterHeight)) {
                var frame = tableFooterView.frame
                frame.size.height = nextHeight
                tableFooterView.frame = frame
                self.tableFooterView = tableFooterView
            }
        }
    }

Thank you !!! That's a very nice way to achieve a behavior I needed. I made a few changes so it can be more SwiftLint compliant :

    func adjustFooterViewHeightToFillTableView() {
        // Invoke from UITableViewController.viewDidLayoutSubviews()

        guard let tableFooterView = self.tableFooterView else { return }

        let minHeight = tableFooterView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
        let currentFooterHeight = tableFooterView.frame.height
        let fitHeight = self.frame.height - self.adjustedContentInset.top - self.contentSize.height + currentFooterHeight
        let nextHeight = (fitHeight > minHeight) ? fitHeight : minHeight

        // No height change needed ?
        guard round(nextHeight) != round(currentFooterHeight) else { return }

        var frame = tableFooterView.frame
        frame.size.height = nextHeight
        tableFooterView.frame = frame
        self.tableFooterView = tableFooterView
    }

@jonasz-baron
Copy link

Edited @Heidan34's comment to take into account Safe Area insets:

func adjustFooterViewHeightToFillTableView() {
        // Invoke from UITableViewController.viewDidLayoutSubviews()

        guard let tableFooterView = self.tableFooterView else { return }

        let minHeight = tableFooterView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
        let currentFooterHeight = tableFooterView.frame.height
        let safeAreaBottomHeight = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0.0
        let fitHeight = self.frame.height - self.adjustedContentInset.top - self.contentSize.height + currentFooterHeight - safeAreaBottomHeight
        let nextHeight = (fitHeight > minHeight) ? fitHeight : minHeight

        // No height change needed ?
        guard round(nextHeight) != round(currentFooterHeight) else { return }

        var frame = tableFooterView.frame
        frame.size.height = nextHeight
        tableFooterView.frame = frame
        self.tableFooterView = tableFooterView
}

@nathanhosselton
Copy link

nathanhosselton commented Aug 18, 2020

Prior solutions were additive to the footer height when viewDidLayoutSubviews is called multiple times, causing the footer to become larger than intended. Maybe it worked fine in a UITableView subclass/extension, but not in a UITableViewController (my usage).

Here's the updated logic applied directly in a UITableViewController context:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    // Fill any empty content space with the footer view, pushing it to the bottom of the screen.
    fillContentGap:
    if let tableFooterView = tableView.tableFooterView {
        /// The expected height for the footer under autolayout.
        let footerHeight = tableFooterView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
        /// The amount of empty space to fill with the footer view.
        let gapHeight: CGFloat = tableView.bounds.height - tableView.adjustedContentInset.top - tableView.adjustedContentInset.bottom - tableView.contentSize.height
        // Ensure there is space to be filled
        guard gapHeight.rounded() > 0 else { break fillContentGap }
        // Fill the gap
        tableFooterView.frame.size.height = gapHeight + footerHeight
    }
}

Of course, make sure your constraints on your footer content are sufficient to keep it pinned to the view's bottom edge as its height expands.

It's also worth noting that the Swift compiler has a time with the lengthy gapHeight calculation, which is why I explicitly declared its type. In my usage I have an extension var vertical: CGFloat { top + bottom } on UIEdgeInsets which helps reduce this line a bit more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment