Search code examples
iosuitableviewautolayoutios-autolayout

Prevent UIStackView from compressing UITableView


I am adding a UITableView into vertical UIStackView. That vertical UIStackView is within a UIScrollView.

However the table is not displaying unless I force an explicit Height constraint on it which I obviously don't want to do.

According to this SO question and answer UITableView not shown inside UIStackView this is because "stackview tries to compress content as much as possible"

If I add a UILabel to the StackView it is displayed fine. There is something specific about the UITableView that means it is not. I am using Xamarin and creating the UITableView in code

this.recentlyOpenedPatientsTable = new UITableView()
{
    RowHeight = UITableView.AutomaticDimension,
    EstimatedRowHeight = 44.0f,
    AllowsMultipleSelectionDuringEditing = false,
    TranslatesAutoresizingMaskIntoConstraints = false,
    Editing = false,
    BackgroundColor = UIColor.Clear,
    TableFooterView = new UIView(),
    ScrollEnabled = false,
};   

The UIScrollView is pinned to the Top, Bottom, Left and Right of the View and works fine. It takes the Height I expect.

I have tried both the suggestions in this SO question and neither have worked. I find it odd that I cannot find others having this issue.

Any other suggestions?


Solution

  • Here is a very basic example, using a UITableView subclass to make it auto-size its height based on its content.

    The red buttons (in a horizontal stack view) are the first arranged subView in the vertical stack view.

    The table is next (green background for the cells' contentView, yellow background for a multi-line label).

    And the last arranged subView is a cyan background UILabel:

    enter image description here

    Note that the vertical stack view is constrained 40-pts from Top, Leading and Trailing, and at least 40-pts from the Bottom. If you add enough rows to the table to exceed the available height, you'll have to scroll to see the additional rows.

    //
    //  TableInStackViewController.swift
    //
    //  Created by Don Mag on 6/24/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 TableInStackCell: UITableViewCell {
    
        let theLabel: UILabel = {
            let v = UILabel()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = .yellow
            v.textAlignment = .left
            v.numberOfLines = 0
            return v
        }()
    
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
    
            contentView.backgroundColor = .green
    
            contentView.addSubview(theLabel)
    
            NSLayoutConstraint.activate([
    
                theLabel.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor, constant: 0.0),
                theLabel.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: 0.0),
                theLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor, constant: 0.0),
                theLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor, constant: 0.0),
    
                ])
    
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
    
    }
    
    class TableInStackViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
        let theStackView: UIStackView = {
            let v = UIStackView()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.axis = .vertical
            v.alignment = .fill
            v.distribution = .fill
            v.spacing = 8
            return v
        }()
    
        let addButton: UIButton = {
            let v = UIButton()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.setTitle("Add a Row", for: .normal)
            v.backgroundColor = .red
            return v
        }()
    
        let deleteButton: UIButton = {
            let v = UIButton()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.setTitle("Delete a Row", for: .normal)
            v.backgroundColor = .red
            return v
        }()
    
        let buttonsStack: UIStackView = {
            let v = UIStackView()
            v.axis = .horizontal
            v.alignment = .fill
            v.distribution = .fillEqually
            v.spacing = 20
            return v
        }()
    
        let theTable: ContentSizedTableView = {
            let v = ContentSizedTableView()
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
    
        let bottomLabel: UILabel = {
            let v = UILabel()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = .cyan
            v.textAlignment = .center
            v.numberOfLines = 0
            v.text = "This label is the last element in the stack view."
            // prevent label from being compressed when the table gets too tall
            v.setContentCompressionResistancePriority(.required, for: .vertical)
            return v
        }()
    
        var theTableData: [String] = [
            "Content Sized Table View",
            "This row shows that the cell heights will auto-size, based on the cell content (multi-line label in this case).",
            "Here is the 3rd default row",
        ]
    
        var minRows = 1
    
        let reuseID = "TableInStackCell"
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            minRows = theTableData.count
    
            view.addSubview(theStackView)
    
            NSLayoutConstraint.activate([
    
                // constrain stack view 40-pts from top, leading and trailing
                theStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
                theStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40.0),
                theStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40.0),
    
                // constrain stack view *at least* 40-pts from bottom
                theStackView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40.0),
    
                ])
    
            buttonsStack.addArrangedSubview(addButton)
            buttonsStack.addArrangedSubview(deleteButton)
    
            theStackView.addArrangedSubview(buttonsStack)
            theStackView.addArrangedSubview(theTable)
            theStackView.addArrangedSubview(bottomLabel)
    
            theTable.delegate = self
            theTable.dataSource = self
    
            theTable.register(TableInStackCell.self, forCellReuseIdentifier: reuseID)
    
            addButton.addTarget(self, action: #selector(addRow), for: .touchUpInside)
            deleteButton.addTarget(self, action: #selector(deleteRow), for: .touchUpInside)
    
        }
    
        @objc func addRow() -> Void {
            // add a row to our data source
            let n = theTableData.count - minRows
            theTableData.append("Added Row: \(n + 1)")
            theTable.reloadData()
        }
    
        @objc func deleteRow() -> Void {
            // delete a row from our data source (keeping the original rows intact)
            let n = theTableData.count
            if n > minRows {
                theTableData.remove(at: n - 1)
                theTable.reloadData()
            }
        }
    
        func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
    
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return theTableData.count
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: reuseID, for: indexPath) as! TableInStackCell
            cell.theLabel.text = theTableData[indexPath.row]
            return cell
        }
    
    }