I have an issue with the self-sizing cell in UITableView in Swift, iOS 15.
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:
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".
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
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)
}
}
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()
}
}
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
}
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
.