0

I have read a lot about how to make table cells' heights automatically adjust according to the text in the cell. From this, this and this, it seems like UITableView.automaticDimension is the way to go. And apparently it works with system (i.e. non-custom) cells as well.

In my table view, I am using a bunch of system cells of the style "Right Detail" (note that this is not my custom cell):

enter image description here

I made both labels have a "Lines" of 0 and a "Line Break" of "Word Wrap" to enable word wrap. Now in my table VC, I populated the table view with some data that are designed to show the problem that I'm having:

var strings = [("Some Very Long Black Text That Doesn't Fit. Foo Bar Baz.", ""),
               ("A", "Short Text"),
               ("B", "Foo Bar"),
               ("Not working:", "Some Very Long Grey Text That Doesn't Fit. Foo Bar Baz"),
               ("C", "Another Text")]

override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return strings.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
    cell?.textLabel?.text = strings[indexPath.row].0
    cell?.detailTextLabel?.text = strings[indexPath.row].1
    return cell!
}

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return UITableView.automaticDimension
}

This is the result:

enter image description here

As you can see, the first row correctly adjusts its height to fit the label, but the fourth row's label overflows its cell. It seems like the table view only tries to fit the row height to textLabel.text, without considering the text in detailTextLabel at all.

How can I make the table view consider both label's text when figuring out the cell height?


Additional attempt:

I thought I could create a custom cell that is very similar to the system "Right Detail" cell style and use that cell instead. Whatever I do, I can't recreate the system cell's constraints. Here's my best attempt, for example.

enter image description here

enter image description here

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • You have a couple issues. First, don't try to modify the properties of a built-in cell type - almost always causes other problems. Second, you have to decide how you want to handle cases where one cell has long text on both the left and right side. Do you want the left or right label to take priority? Do you want to limit the labels to, say, 2/3rds of the width of the cell? Depending on your answers, you may need to write some additional code logic to determine which side gets what width. – DonMag Jan 20 '20 at 13:04
  • Oh, ok. I didn't know I was not supposed to modify the properties of a built-in cell type. I mean I was just changing the styles of the labels... As for what I want when both labels have long text, I would like each label to occupy a half of the cell's width. @DonMag – Sweeper Jan 20 '20 at 14:53

1 Answers1

5

It's going to be difficult to get this to work exactly as you want.

Auto-layout makes multiple "passes" to try and satisfy layout requirements. However, think about what it has to do:

  • Set the text of both labels
  • Does "leftLabel" need to wrap?
  • Does "rightLabel" need to wrap?
  • If "leftLabel" wraps, does "rightLabel" still need to wrap?
  • and vice-versa?

And, at what point in that process could it be determined that both labels will end up wrapping, so make each one 50% of the width? Keeping in mind that the cell width will vary, depending on device and orientation?

You may want to look at the actual data you expect to be displaying, and think about how it will actually look. A "right detail cell" design may not end up as the best layout.

That said - here is a custom cell that might work for you. It will need plenty of edge-testing to see if any sizing gets thrown off (for example, by very long strings and/or by very big differentials between string lengths):

class MyRightDetailCell: UITableViewCell {

    var myTextLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.numberOfLines = 0
        v.textAlignment = .left
        v.font = UIFont.systemFont(ofSize: 17.0)
        v.textColor = .black
        v.setContentHuggingPriority(.required, for: .horizontal)
        v.setContentHuggingPriority(.required, for: .vertical)
        v.setContentCompressionResistancePriority(.required, for: .horizontal)
        v.setContentCompressionResistancePriority(.required, for: .vertical)
        return v
    }()

    var myDetailTextLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.numberOfLines = 0
        v.textAlignment = .right
        v.font = UIFont.systemFont(ofSize: 17.0)
        v.textColor = .darkGray
        v.setContentHuggingPriority(.required, for: .horizontal)
        v.setContentHuggingPriority(.required, for: .vertical)
        v.setContentCompressionResistancePriority(.required, for: .horizontal)
        v.setContentCompressionResistancePriority(.required, for: .vertical)
        return v
    }()

    var theStackView: UIStackView = {
        let v = UIStackView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.axis = .horizontal
        v.alignment = .top
        v.distribution = .fill
        v.spacing = 8
        v.setContentHuggingPriority(.required, for: .vertical)
        v.setContentCompressionResistancePriority(.required, for: .vertical)
        return v
    }()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    func commonInit() -> Void {

        contentView.addSubview(theStackView)
        theStackView.addArrangedSubview(myTextLabel)
        theStackView.addArrangedSubview(myDetailTextLabel)

        let g = contentView.layoutMarginsGuide

        NSLayoutConstraint.activate([
            theStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            theStackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            theStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            theStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
        ])

    }

}

class RightDetailTableViewController: UITableViewController {

    var strings = [
        ("Some Very Long Black Text That Doesn't Fit. Foo Bar Baz.", ""),
        ("A", "Short Text"),
        ("B Somewhat Much Longer Left Label Text.", "With long right label text."),
        ("Working Now?", "Some Very Long Grey Text (Three Lines?) That Doesn't Fit. Foo Bar Baz"),
        ("C", "Another Text"),
        ("D", "With Long Right Label Text That Will Need To Wrap."),
    ]

    let cellID = "MYRDC"

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.register(MyRightDetailCell.self, forCellReuseIdentifier: cellID)

        // make 5 copies of the test data so we'll have plenty of rows
        // for scrolling (to check cell re-use)
        for _ in 1...5 {
            strings.append(contentsOf: strings)
        }
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        // due to the complexity of the cell, the layout is more reliable if the
        // table is reloaded here - try it with and without this extra reloadData() call
        tableView.reloadData()
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return strings.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! MyRightDetailCell

        cell.myTextLabel.text = strings[indexPath.row].0
        cell.myDetailTextLabel.text = strings[indexPath.row].1

        // if either label has no text (""), set it to hidden
        // to remove the stack view's spacing
        cell.myTextLabel.isHidden = (strings[indexPath.row].0 == "")
        cell.myDetailTextLabel.isHidden = (strings[indexPath.row].1 == "")

        // un-comment the next two lines to show label background colors
        // to make it easy to see the label frames
        //cell.myTextLabel.backgroundColor = .cyan
        //cell.myDetailTextLabel.backgroundColor = .green

        return cell
    }

}

Output:

enter image description here

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • "You may want to look at the actual data you expect to be displaying, and think about how it will actually look." Regarding this, I'm going to display the column names of a table in a database on the left side, and the values of a record on the right. So there will likely be a lot more wrapping on the right than on the left. – Sweeper Jan 20 '20 at 16:27
  • The thing that I want Autolayout to do (but can't express it in code) is to first stretch the left label to half the width of the cell. Set its text. If the text doesn't wrap, then resize the label to fit the text. After that, use however much width is left for the right label. But probably "Auto"layout doesn't allow so much "manual" instructions of what to do... – Sweeper Jan 20 '20 at 16:30