swiftuitableviewuikit

UILabel in self-sizing UITableView Cell calculate height randomly


I have an issue with the self-sizing cell in UITableView in Swift, iOS 15.

I want to create a cell like here:

Expected cell

And it works fine when the view renders 80% of the cells. But sometimes I have something like this. It''s how it looks in view debugger:

Cell with issue in view of debugger

As you can see, in a few cells, the height for UILabel (nameLabel in my code, which is below), is much what the view need. I want to use self-sizing cells to support accessibility, but in this case, some cells take up more space than they need. What is more, when I scroll down and get back to the top, when cells rerender by UI some cells got fixed.

Also, view debugger shows me the error "Scrollable content size is ambigous for UITableView".

What I tried to do:

I tried to transfer the code below to UIStackView, I mean nameLabel and teamLabel, but the problem with the height of teamLabel still occurs. I have no idea how to fix this. I tried using .setContentHuggingPriority, but without much effect.

The data I use is downloaded in memory before opening the view, so there is certainly no situation where the view is unable to calculate what size it should be.

When I remove teamLabel, the height of nameLabel looks correct. Despite this, the "purple" error in the debugger still occurs. I checked all the view constraints and each side of the cell has references to the elements. Generally, the cell displays correctly, also with a larger font, if I change the accessibility, but I don't know how to fix the problem with this one UILabel.

I would be very grateful for any tips. I have absolutely no idea how to make it work properly. What is important, code about hyphenation change nothing - i still have issue without it. I tried add "tableview.estimatedHeightRow" but without expected effect

My custom cell code:

import UIKit

class RaceResultCell: UITableViewCell {
    static let identifier = "RaceResultDetailsCell"
    
    private let flagImage = FlagImageView(frame: .zero)
    
    private let positionLabel = CellTextLabel(fontStyle: .subheadline, fontWeight: .regular, textColor: .UI.secondaryText, textAlignment: .center)
    private let nameLabel = CellTextLabel(fontStyle: .body, fontWeight: .semibold, textColor: .UI.primaryText)
    
    private let teamLabel = CellTextLabel(fontStyle: .subheadline, fontWeight: .light, textColor: .UI.secondaryText)
    
    private let statusLabel = CellTextLabel(fontStyle: .subheadline, fontWeight: .regular, textColor: .UI.primaryText, textAlignment: .right)
    private let pointsLabel = CellTextLabel(fontStyle: .subheadline, fontWeight: .regular, textColor: .UI.secondaryText, textAlignment: .center)
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    
        setupUI()
    }

    
    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configure(with model: RaceResultDataModel) {
        positionLabel.text = model.position
        nameLabel.text = "\(model.driver.name) \(model.driver.surname)"
        pointsLabel.text = model.points
        teamLabel.text = model.constructor.name
        statusLabel.text = model.time?.time ?? model.status
        
        flagImage.image = CountryFlagProvider.shared.nationalityFlag(for: model.driver.nationality)
        
        setupHyphenation(for: nameLabel)
        setupHyphenation(for: teamLabel)
        setupHyphenation(for: statusLabel)
    }
    
    private func setupHyphenation(for label: UILabel) {
         label.numberOfLines = 0
         label.lineBreakMode = .byWordWrapping
         label.adjustsFontSizeToFitWidth = false
         label.allowsDefaultTighteningForTruncation = true
         if let text = label.text {
             let paragraphStyle = NSMutableParagraphStyle()
             paragraphStyle.hyphenationFactor = 1
             let attributedString = NSAttributedString(string: text,
                                                       attributes: [.paragraphStyle: paragraphStyle])
             label.attributedText = attributedString
         }
     }

    private func setupUI() {
        contentView.addSubview(positionLabel)
        contentView.addSubview(flagImage)
        contentView.addSubview(nameLabel)
        contentView.addSubview(teamLabel)
        contentView.addSubview(statusLabel)
        contentView.addSubview(pointsLabel)

        positionLabel.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(6)
            make.centerY.equalToSuperview()
            make.width.greaterThanOrEqualTo(28)
        }
        
        flagImage.snp.makeConstraints { make in
            make.leading.equalTo(positionLabel.snp.trailing).offset(12)
            make.centerY.equalToSuperview()
            make.size.equalTo(CGSize(width: 28, height: 24))

        }
        
        nameLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(12)
            make.leading.equalTo(flagImage.snp.trailing).offset(12)
            make.trailing.lessThanOrEqualTo(statusLabel.snp.leading).offset(-12)
        }
                
        
        teamLabel.snp.makeConstraints { make in
            make.top.equalTo(nameLabel.snp.bottom).offset(6)
            make.leading.equalTo(flagImage.snp.trailing).offset(12)
            make.trailing.lessThanOrEqualTo(statusLabel.snp.leading).offset(-12)
            make.bottom.equalToSuperview().offset(-12)
        }
        
        statusLabel.snp.makeConstraints { make in
            make.centerY.equalToSuperview()
            make.trailing.equalTo(pointsLabel.snp.leading).offset(-12)
            make.top.equalTo(nameLabel.snp.top)
            make.bottom.equalTo(teamLabel.snp.bottom)
            make.width.greaterThanOrEqualTo(50)

        }
        
        pointsLabel.snp.makeConstraints { make in
            make.trailing.equalToSuperview().offset(-6)
            make.centerY.equalToSuperview()
            make.width.greaterThanOrEqualTo(28)
        }       
//        
//    
//        nameLabel.setContentHuggingPriority(.defaultHigh + 1, for: .vertical)
//        teamLabel.setContentHuggingPriority(.defaultHigh + 2, for: .vertical)
//        
//        nameLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .vertical)

    }
}

UILabel which i use:

import UIKit

class CellTextLabel: UILabel {
    override init(frame: CGRect) {
        super.init(frame: frame)
        configure()
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    convenience init(fontStyle: UIFont.TextStyle, fontWeight: UIFont.Weight, textColor: UIColor!, textAlignment: NSTextAlignment? = nil) {
        self.init(frame: .zero)
        self.font = UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: fontStyle).pointSize, weight: fontWeight)
        self.textColor = textColor
        self.textAlignment = textAlignment ?? .natural
    }

    private func configure() {
        adjustsFontForContentSizeCategory = true
        minimumScaleFactor = 0.5
        lineBreakMode = .byWordWrapping
        numberOfLines = 0
        sizeToFit()
    }
}

UITableView setup:

 private func setupTableView() {
        view.addSubview(tableView)

        tableView.delegate = self
        tableView.dataSource = self
        tableView.backgroundColor = UIColor.UI.background
        tableView.allowsSelection = false
        tableView.register(RaceResultCell.self, forCellReuseIdentifier: RaceResultCell.identifier)
        tableView.isHidden = false
        tableView.showsVerticalScrollIndicator = false
        
        tableView.addSubview(tableViewHeader)
        tableViewHeader.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 50)
        tableViewHeader.backgroundColor = UIColor.UI.background
        tableView.tableHeaderView = tableViewHeader

        tableViewHeader.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.left.equalToSuperview().offset(10)
            make.right.equalToSuperview().offset(-10)
            make.height.equalTo(50)
        }
        
        tableView.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide.snp.top)
            make.left.right.bottom.equalToSuperview()
        }
        
    }


/// HEIGHT & CELL SETUP IN EXTENSION:

extension ResultsDetailsVC: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // count setup...
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: RaceResultCell.identifier, for: indexPath) as? RaceResultCell else {
            fatalError("Custom cell error")
        }
        
        let resultData = raceResult.results[indexPath.row]
        cell.configure(with: resultData)
        
        return cell
    }
    
    func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat {
        return UITableView.automaticDimension
    }

Solution

  • I believe the issue lies with the multiple usage of greaterThanOrEqualTo on the label width constraints, and auto-layout is having trouble evaluating the Heights of the multi-line labels, because their Widths are changing as the layout is begin evaluated.

    Try this:

        positionLabel.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(6)
            make.centerY.equalToSuperview()
            make.width.greaterThanOrEqualTo(28)
        }
        
        flagImage.snp.makeConstraints { make in
            make.leading.equalTo(positionLabel.snp.trailing).offset(12)
            make.centerY.equalToSuperview()
            make.size.equalTo(CGSize(width: 28, height: 24))
        }
        
        nameLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(12)
            make.leading.equalTo(flagImage.snp.trailing).offset(12)
            make.trailing.equalTo(statusLabel.snp.leading).offset(-12)
        }
        
        
        teamLabel.snp.makeConstraints { make in
            make.top.equalTo(nameLabel.snp.bottom).offset(6)
            make.leading.equalTo(flagImage.snp.trailing).offset(12)
            make.trailing.equalTo(statusLabel.snp.leading).offset(-12)
            make.bottom.equalToSuperview().offset(-12)
        }
        
        statusLabel.snp.makeConstraints { make in
            make.centerY.equalToSuperview()
            make.trailing.equalTo(pointsLabel.snp.leading).offset(-12)
            make.top.equalTo(nameLabel.snp.top)
            make.bottom.equalTo(teamLabel.snp.bottom)
            make.width.greaterThanOrEqualTo(50)
        }
        
        pointsLabel.snp.makeConstraints { make in
            make.trailing.equalToSuperview().offset(-6)
            make.centerY.equalToSuperview()
            make.width.greaterThanOrEqualTo(28)
        }
    
        // add these constraints to help auto-layout manage the labels
        var c: NSLayoutConstraint!
        
        c = positionLabel.widthAnchor.constraint(equalToConstant: 28.0)
        c.priority = .defaultHigh
        c.isActive = true
        
        c = statusLabel.widthAnchor.constraint(equalToConstant: 50.0)
        c.priority = .defaultHigh
        c.isActive = true
        
        c = pointsLabel.widthAnchor.constraint(equalToConstant: 28.0)
        c.priority = .defaultHigh
        c.isActive = true
    

    We are effectively giving auto-layout hints at the variable width positionLabel and statusLabel and pointsLabel labels.

    We've also changed nameLabel and teamLabel trailing constraints from lessThanOrEqualTo to equalTo.