How to correctly get tableViewCell's contentView bound size?
Asked Answered
S

1

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.

Sorenson answered 8/10, 2018 at 18:11 Comment(4)
Maybe overriding a layout method in MyTableViewCell like layoutSubviews and setting preferredMaxLayoutWidth there instead of in cellForRowAt: would workTarpley
Shouldn't the current sequence of my code have the same effect?Sorenson
Ignoring the bounds changes, what are you trying to accomplish? It's very possible you don't need that value to begin with...Homotaxis
@Homotaxis This is image1 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 perfectSorenson
H
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.

Homotaxis answered 8/10, 2018 at 20:55 Comment(10)
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...Sorenson
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 :/Sorenson
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)Goldberg
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.Goldberg
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?Sorenson
@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.Homotaxis
@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.Homotaxis
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 are pointing to use itSorenson
@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.Homotaxis
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 and was later doing something similar myself.Sorenson

© 2022 - 2024 — McMap. All rights reserved.