skip to Main Content

I have UITableView with one section (1 header view + 100 rows). Its vertical constraints are to top view’s bottom and to superview’s bottom (not bottom safe area).

I provided all the necessary heights manually with UITableViewDelegate:

  • section header height
  • first and last cell height
  • other cells height

If I sum all these heights i get ~6000px but tableView.contentSize.height returns ~4500 until I scroll to the bottom of the table. And this happens even with default cells.

I understand that I can calculate everything manually but why table has wrong content size if all its inner element heights are predefined?

2

Answers


  1. Chosen as BEST ANSWER

    Found a solution for my case (when all the table items' heights are predefined): https://developer.apple.com/forums/thread/81895

    It seems heightForRow delegate method determines the height of the row which is drawn/visible on the screen but is not properly involved in contentSize calculation.

    estimatedHeightForRowAt looks like the opposite method. It determines the height of the row for contentSize calculation but without of heightForRow visible rows have the default height.

    So it is necessary to implement both heightForRow and estimatedHeightForRowAt methods for this case.

    The same is for header/footer heights (if these items are displayed)


  2. A UITableView – like most components – does a lot of "behind the scenes" work. When it loads, it doesn’t need to calculate or render its entire potential content size.

    Quick example to demonstrate…

    simple single label cell class

    class ContentSizeTableCell: UITableViewCell {
        
        static let identifier: String = "ContentSizeTableCell"
        
        let label = UILabel()
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            label.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(label)
            let g = contentView.layoutMarginsGuide
            NSLayoutConstraint.activate([
                label.topAnchor.constraint(equalTo: g.topAnchor),
                label.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                label.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                label.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            ])
            // so we can see the framing
            label.backgroundColor = .yellow
        }
        
    }
    

    basic table view controller

    class ContentSizeTableVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
        
        var tableView: UITableView!
        
        let nSections: Int = 1
        let nRows: Int = 100
    
        // track the rows being asked for height
        var heightForRowMaxRow: Int = -1
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            tableView = UITableView(frame: .zero, style: .plain)
            tableView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(tableView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
                
            ])
            
            tableView.register(ContentSizeTableCell.self, forCellReuseIdentifier: ContentSizeTableCell.identifier)
            tableView.dataSource = self
            tableView.delegate = self
            
        }
        
        func numberOfSections(in tableView: UITableView) -> Int {
            return nSections
        }
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return nRows
        }
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let c = tableView.dequeueReusableCell(withIdentifier: ContentSizeTableCell.identifier, for: indexPath) as! ContentSizeTableCell
            c.label.text = "(indexPath)"
            return c
        }
    
        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            heightForRowMaxRow = max(heightForRowMaxRow, indexPath.row)
            
            // return 100 for 1st and last row
            //  50 for all other rows
            if indexPath.row == 0 ||
                indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1
            {
                return 100.0
            }
            return 50.0
        }
        
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            print("hfrMax:", heightForRowMaxRow, tableView.contentSize.height)
        }
    }
    

    Note in the controller class, we have this property:

    // track the rows being asked for height
    var heightForRowMaxRow: Int = -1
    

    and we’ve implemented heightForRowAt like this:

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        heightForRowMaxRow = max(heightForRowMaxRow, indexPath.row)
        
        // return 100 for 1st and last row
        //  50 for all other rows
        if indexPath.row == 0 ||
            indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1
        {
            return 100.0
        }
        return 50.0
    }
    

    Every time heightForRowAt is called, we’ll set that property to the highest row number.

    In didSelectRowAt, we’ll print that property and the table view’s .contentSize.height.

    On launch, on an iPhone 14 Pro, 14 rows are visible. Selecting a row outputs this to the debug console:

    hfrMax: 14 4540.0
    

    As we see, heightForRowAt has been called only for the first 14 rows.

    Scrolling a bit and selecting rows outputs this:

    hfrMax: 33 4654.0
    hfrMax: 66 4852.0
    hfrMax: 99 5100.0
    

    Once we’ve scrolled far enough so that heightForRowAt has been called on ALL the rows, we now have a valid tableView.contentSize.height.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search