0

Resources:

I've read multiple answers from Using Auto Layout in UITableView for dynamic cell layouts & variable row heights

And followed their suggestions but it's not working.

Setup to reproduce:

If you copy/paste the MyTableViewCell and ViewController snippets: then you can reproduce the issue.

I have subclassed MyTableViewCell and added my own label.

    import UIKit

    class MyTableViewCell: UITableViewCell {

        lazy var customLabel : UILabel = {
            let lbl = UILabel()
            lbl.translatesAutoresizingMaskIntoConstraints = false
            lbl.numberOfLines = 0
            return lbl
        }()

        override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            setupLayout()
        }
        private func setupLayout(){
            contentView.addSubview(customLabel)

            let top = customLabel.topAnchor.constraint(equalTo: contentView.topAnchor)
            let bottom = customLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
            let leadingFromImage = customLabel.leadingAnchor.constraint(equalTo: imageView!.trailingAnchor, constant: 5)
            let trailing = customLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)

            NSLayoutConstraint.activate([top, bottom, leadingFromImage, trailing])
        }

        required init?(coder aDecoder: NSCoder) {
            fatalError()
        }
    }

The following ViewController contains my tableview:

import UIKit

class ViewController: UIViewController {

    var datasource = ["It would have been a great day had Manchester United Lost its \n game. Anyhow I hope tomorrow Arsenal will win the game"]

    lazy var tableView : UITableView = {
        let table = UITableView()
        table.delegate = self
        table.dataSource = self
        table.translatesAutoresizingMaskIntoConstraints = false
        table.estimatedRowHeight = 100
        table.rowHeight = UITableViewAutomaticDimension
        return table
    }()
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(tableView)
        tableView.pinToAllEdges(of: view)
        tableView.register(MyTableViewCell.self, forCellReuseIdentifier: "id")
    }
}

extension ViewController: UITableViewDelegate, UITableViewDataSource {

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

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

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

        cell.customLabel.text = datasource[indexPath.row]
        logInfo(of: cell)

        cell.accessoryType = .detailDisclosureButton
        cell.imageView?.image = UIImage(named: "honey")
        cell.layoutSubviews()
        cell.customLabel.preferredMaxLayoutWidth = tableView.bounds.width
        logInfo(of: cell)
        print("---------")

        return cell
    }

    private func logInfo(of cell: MyTableViewCell){
        print("boundsWidth: \(cell.contentView.bounds.width) | maxLayoutWidth: \(cell.contentView.bounds.width - 44 - 15 - 5) | systemLayoutSizeFitting : \(cell.customLabel.systemLayoutSizeFitting(UILayoutFittingCompressedSize))")
    }    
}

extension UIView{

    func pinToAllEdges(of view: UIView){
        let leading = leadingAnchor.constraint(equalTo: view.leadingAnchor)
        let top = topAnchor.constraint(equalTo: view.topAnchor)
        let trailing = trailingAnchor.constraint(equalTo: view.trailingAnchor)
        let bottom = bottomAnchor.constraint(equalTo: view.bottomAnchor)        

        NSLayoutConstraint.activate([leading, top, trailing, bottom])
    }
}

Link for honey image I used. I've set it's size to 44 * 44

Main issue

My major problem is inside cellForRowAtIndex:

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

    cell.customLabel.text = datasource[indexPath.row]
    logInfo(of: cell)

    cell.accessoryType = .detailDisclosureButton
    cell.imageView?.image = UIImage(named: "honey")
    cell.layoutSubviews()
    cell.customLabel.preferredMaxLayoutWidth = cell.contentView.bounds.width
    logInfo(of: cell)
    print("---------")

    return cell
}

Questions:

For whatever reason the value assigned to:

cell.customLabel.preferredMaxLayoutWidth

doesn't seem to be right.

Q1: Why is that?

enter image description here

Q2: I'm logging the contentView's bound before and after I call cell.layoutSubviews and it switches from 320 to 260 but then eventually in the viewDebugger it shows up as 308!!!

Why is the contenView's bounds changing again?!

I've removed some other screenshots from the question. They were mostly clutter but maybe worth looking. You can take a look at the revision history.

mfaani
  • 33,269
  • 19
  • 164
  • 293
  • Maybe overriding a layout method in `MyTableViewCell` like `layoutSubviews` and setting `preferredMaxLayoutWidth` there instead of in `cellForRowAt:` would work – Alex Oct 08 '18 at 19:22
  • Shouldn't the current sequence of my code have the same effect? – mfaani Oct 08 '18 at 19:27
  • Ignoring the bounds changes, what are you trying to accomplish? It's very possible you don't need that value to begin with... – DonMag Oct 08 '18 at 19:59
  • @DonMag This is [image1](https://i.stack.imgur.com/byAQT.png) from the initial version of the question. That's what happens if I don't set any width constraint on `customLabel` it just has 4 constraints to the sides. Using the `preferredMaxLayoutWidth` I'm trying to set a width for it. So the label's height would **drive the maximum height needed for that cell**. I assigned the `cell.contentView.bounds.width` to the label's `preferredMaxLayoutWidth`. But that's not working accurately. So I assigned `preferredMaxLayoutWidth` after I called `layoutSubviews`. UI looked better but not perfect – mfaani Oct 08 '18 at 20:15

1 Answers1

1

I believe the issue is related to using the default cell's imageView.

The image view itself doesn't exist until its .image property is set, so on your cell init you're constraining the custom label to an image view that is 0,0,0,0

Then, in cellForRowAt, you set the .image property, and it appears that action also sets the contentView height. I can't find any docs on it, and digging through in debug I can't find any conflicting constraints, so I'm not entirely sure why that's happening.

Two options:

1 - Instead of creating and adding a custom label, set the .numberOfLines on the default .textLabel to 0. That should be enough.

2 - If you need a customized label, also add a custom image view.

Option 2 is here:

class MyTableViewCell: UITableViewCell {

    lazy var customLabel : UILabel = {
        let lbl = UILabel()
        lbl.translatesAutoresizingMaskIntoConstraints = false
        lbl.numberOfLines = 0
        lbl.setContentCompressionResistancePriority(.required, for: .vertical)
        return lbl
    }()

    lazy var customImageView: UIImageView = {
        let v = UIImageView()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupLayout()
    }

    private func setupLayout(){
        contentView.addSubview(customLabel)

        contentView.addSubview(customImageView)

        // constrain leading of imageView to be 15-pts from the leading of the contentView
        let imgViewLeading = customImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15)

        // constrain width of imageView to 42-pts
        let imgViewWidth = customImageView.widthAnchor.constraint(equalToConstant: 42)

        // constrain height of imageView to be equal to width of imageView
        let imgViewHeight = customImageView.heightAnchor.constraint(equalTo: customImageView.widthAnchor, multiplier: 1.0)

        // center imageView vertically
        let imgViewCenterY = customImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0.0)

        // top and bottom constraints for the imageView also need to be set,
        // otherwise the image will exceed the height of the cell when there
        // is not enough text to wrap and expand the height of the label

        // constrain top of imageView to be *at least* 4-pts from the top of the cell
        let imgViewTop = customImageView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 4)

        // constrain bottom of imageView to be *at least* 4-pts from the bottom of the cell
        let imgViewBottom = customImageView.topAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -4)

        // constrain top of the label to be *at least* 4-pts from the top of the cell
        let top = customLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4)

        // if you want the text in the label vertically centered in the cell
        // constrain bottom of the label to be *exactly* 4-pts from the bottom of the cell
        let bottom = customLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4)

        // if you want the text in the label top-aligned in the cell
        // constrain bottom of the label to be *at least* 4-pts from the bottom of the cell
        // let bottom = customLabel.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -4)

        // constrain leading of the label to be 5-pts from the trailing of the image
        let leadingFromImage = customLabel.leadingAnchor.constraint(equalTo: customImageView.trailingAnchor, constant: 5)

        // constrain the trailing of the label to the trailing of the contentView
        let trailing = customLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)

        NSLayoutConstraint.activate([
            top, bottom, leadingFromImage, trailing,
            imgViewLeading, imgViewCenterY, imgViewWidth, imgViewHeight,
            imgViewTop, imgViewBottom
            ])

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }
}

class HoneyViewController: UIViewController {

    var datasource = [
        "It would have been a great day had Manchester United Lost its game. Anyhow I hope tomorrow Arsenal will win the game",
        "One line.",
        "Two\nLines.",
    ]

    lazy var tableView : UITableView = {
        let table = UITableView()
        table.delegate = self
        table.dataSource = self
        table.translatesAutoresizingMaskIntoConstraints = false
        table.estimatedRowHeight = 100
        table.rowHeight = UITableViewAutomaticDimension
        return table
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(tableView)
        tableView.pinToAllEdges(of: view)
        tableView.register(MyTableViewCell.self, forCellReuseIdentifier: "id")
    }
}

extension HoneyViewController: UITableViewDelegate, UITableViewDataSource {

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

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

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

        cell.customLabel.text = datasource[indexPath.row]
        logInfo(of: cell)

        cell.accessoryType = .detailDisclosureButton
        cell.customImageView.image = UIImage(named: "Honey")
        logInfo(of: cell)
        print("---------")

        return cell
    }

    private func logInfo(of cell: MyTableViewCell){
        print("boundsWidth: \(cell.contentView.bounds.width) | maxLayoutWidth: \(cell.contentView.bounds.width - 44 - 15 - 5) | systemLayoutSizeFitting : \(cell.customLabel.systemLayoutSizeFitting(UILayoutFittingCompressedSize))")
    }
}

extension UIView{

    func pinToAllEdges(of view: UIView){
        let leading = leadingAnchor.constraint(equalTo: view.leadingAnchor)
        let top = topAnchor.constraint(equalTo: view.topAnchor)
        let trailing = trailingAnchor.constraint(equalTo: view.trailingAnchor)
        let bottom = bottomAnchor.constraint(equalTo: view.bottomAnchor)

        NSLayoutConstraint.activate([leading, top, trailing, bottom])
    }
}

Edit:

A couple more constraints are needed. If the cell has only enough text for one line (no wrapping), the imageView height will exceed the height of the cell:

enter image description here

So, we add top and bottom constraints to the imageView to fit at least the top and bottom of the cell:

enter image description here

and, it will probably look a little better with some padding, so we constrain the top and bottom of the imageView to be at least 4-pts from the top and bottom of the cell:

enter image description here

If desired, we can also "top-align" the text in the label by constraining its bottom to be at least 4-pts from the bottom, instead of exactly 4-pts from the bottom:

enter image description here

The comments in my edited code should explain each of those differences.

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • _Instead of creating and adding a custom label, set the .numberOfLines on the default .textLabel to 0. That should be enough._ Agreed. But I don't want to do that. I want to solve **this** issue. Get to the bottom of it and see why it's not working :). Let me try _your_ solution... – mfaani Oct 08 '18 at 20:58
  • I actually remember when I was coding this, I was thinking how come `imageView!.trailingAnchor` didn't fail. I mean if it's optional and not `nil` upon instantiation and is though empty then it's weird :/ – mfaani Oct 08 '18 at 21:03
  • I really don't understand why you are writing the the layout from code... but nvm that. Create the custom cell in the storyboard or in the xib (if you make a xib you will need to register it on the tableview) @Honey is right make a custom image view and a custom label set the constraints correctly. Set the number of lines to 0, and the line break to 0. And as long you've set the constraints correctly the cell should expand. (Don't forget to set cell row height to automatically and estimated to you desired number) – Jakub Sanestrzik Oct 08 '18 at 21:16
  • When writing layouts from code its very likely that you make a mistake there, and will spend the next hour figuring out what one wrong. – Jakub Sanestrzik Oct 08 '18 at 21:20
  • Thanks. Your option2 works.1) _you set the .image property, and it appears that action also sets the contentView height_ why do you say that? In the screenshot of my question, isn't the label's height driving the logic? I mean its height is bigger than the image's height, and the cell is expanding 2) I removed your `contentCompressionsResistence` it was working without it. 3) I'm all baffled as to why you'd ever need to use `preferredLayoutWidth`. I mean the if you set the `numberOfLines = 0` **AND** constraint the label's leading/trailing anchors then I can't see a reason for it. Am I right? – mfaani Oct 08 '18 at 21:54
  • @Honey - yeah, I forgot to remove the compression resistance line (was trying it with the previous code). I also don't understand why setting the `.image` property is affecting the contentView's constraint, but that sure seems to be what's happening. As far as `preferredMaxLayoutWidth` goes, as I understand it, that can come into play when explicit width constraints are *not* used... so, one might say "don't wrap the text *unless* it exceeds the `preferredMaxLayoutWidth`" - you can find some discussion on it if you want to investigate further, but in general, it's not needed. – DonMag Oct 08 '18 at 22:42
  • 1
    @JaubSanestrzik - many, many, many reasons to use code instead of (or along with) Storyboards / Interface Builder. The issue in this case does not appear to have anything to do with incorrect constraints. – DonMag Oct 08 '18 at 22:46
  • I've accepted your answer but it would be great if you can incorporate when it's needed to use `preferredMaxLayoutWidth` and when it's not needed. FWIW a lot of answers of this [highly viewed question](https://stackoverflow.com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights) are pointing to use it – mfaani Oct 09 '18 at 02:40
  • @Honey - when to use `preferredMaxLayoutWidth` is really a different question, as it's not needed for your target layout. ***Note:*** the code I had posted needed a few more constraints -- see my edited answer. – DonMag Oct 09 '18 at 12:41
  • Good catch for the one line. My whole attempt on writing this answer was to acquaint myself with different ways you can adjust the label's height. Including how/when you should use `preferredMaxLayoutWidth`. I ran into this question after I wrote [this answer](https://stackoverflow.com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights/52673314#52673314) and was later doing something similar myself. – mfaani Oct 09 '18 at 14:19