TableView calculates wrong estimatedHeightForRowAt
Asked Answered
D

10

9

I'm making a chat like application, where the tableView displays dynamic height cells.


The cells have their views&subviews constrained in the right way

So that the AutoLayout can predict the height of the cells

(Top, Bottom, Leading, Trailing)


But still - as you can see in the video - the scroll indicator bar shows that wrong heights were calculated:

It recalculates the heights when a new row is appearing.

Video: https://youtu.be/5ydA5yV2O-Q

(On the second attempt to scroll down everything is fine)


Code:

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

It is a simple problem. Can someone help me out?

Update 1.0

Added github:

https://github.com/krptia/Test

Disputatious answered 17/1, 2019 at 23:0 Comment(12)
Delete both functions and see what happensSabir
estimatedHeightForRowAt should return some static estimated floatOrigan
To solve this problem I did manual height calculation and used only heightForRowAt.Redivivus
If I delete both functions nothing changes :/Morph
First this in theory could be because of the difference of height and estimated height. However I would guess it is not the case and you are looking for the problem in the wrong direction. Set both values to e.g. 60 and see if the error still appears. If it does then tell me to write below, give me 50 points and see where the problem actually appears (you may have some footer possibly?) :)Tocopherol
Could you share a project with minimum code that reproduces this glitch?Stroganoff
What I am experiencing is the constraints setting exception, which causes automaticDimension to not calculate well. Manual calculations are better controlled, but cumbersome and error prone.Ea
If I set the estimatedHeightForRowAt and heightForRowAt the same values it does work, but I need to have UITableView.automaticDimension returned in heightForRowAt because I do sometimes animated constraint changes to the given cells.Morph
@J.Doe Updated main thread, added github project linkMorph
Interesting, even WhatsApp doesn't do this. I wonder if there is an (easy) solution.Stroganoff
So I have read that I have to avoid using Relative to Margin while doing the constraints. It is a bit better now, but still produces the bug.Morph
@sh-kan well said. estimatedHeightForRowAt is used for temporary cell height untill autolayout calculates the cell height. So return some static value instead of UITableView.automaticDimension.Ulotrichous
A
4

But still - as you can see in the video - the scroll indicator bar shows that wrong heights were calculated:

So what you want is precise content height.

For that purpose, you cannot use static estimatedRowHeight. You should implement more correct estimation like below.

    ...

    var sampleCell: WorldMessageCell?

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.register(UINib(nibName: "WorldMessageCell", bundle: nil), forCellReuseIdentifier: "WorldMessageCell")

        sampleCell = UINib(nibName: "WorldMessageCell", bundle: nil).instantiate(withOwner: WorldMessageCell.self, options: nil)[0] as? WorldMessageCell
    }

    ...

    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        if let cell = sampleCell {
            let text = self.textForRowAt(indexPath)
            // note: this is because of "constrain to margins", which value is actually set after estimation. Do not use them to remove below
            let margin = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 20)
            // without "constrain to margins"
            // let margin = cell.contentView.layoutMargins 
            let maxSize = CGSize(width: tableView.frame.size.width - margin.left - margin.right,
                                 height: CGFloat.greatestFiniteMagnitude)
            let attributes: [NSAttributedString.Key: Any]? = [NSAttributedString.Key.font: cell.messageLabel.font]
            let size: CGRect = (text as NSString).boundingRect(with: maxSize,
                                                                 options: [.usesLineFragmentOrigin], attributes: attributes, context: nil)
            return size.height + margin.top + margin.bottom
        }
        return 100
    }

This is too precise (actually real row height) and maybe slow, but you can do more approximate estimation for optimization.

Afterword answered 25/1, 2019 at 4:50 Comment(0)
E
2

You need to set tableFooterView to empty.

override func viewDidLoad() {
    super.viewDidLoad()
    tableView.tableFooterView = UIView()
    // your staff
}
Ea answered 21/1, 2019 at 3:53 Comment(0)
A
2

The problem is with your estimatedHeightForRowAt method. As the name implies it gives the estimated height to the table so that it can have some idea about the scrollable content until the actual content will be displayed. The more accurate value will result in a more smooth scrolling and height estimation.

You should set this value to big enough so that it can represent the height of your cell with the maximum content. In your case 650 is working fine.

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return 650
}

The result would be far better with this approach.

Also, there is no need to implement delegate method for height until you want a variation on index bases. You can simply set table view property.

tableView.estimatedRowHeight = 650.0
tableView.rowHeight = .automaticDimension

Optimization

One more thing I noticed in your demo project. You've used too many if-else in your cellForRowAtIndexPath which is making it little slower. Try to minimize that. I've done some refinement to this, and it improves the performance.

  1. Define an array which holds your message text.

    var messages = ["Lorem ipsum,"many more",.....]

  2. Replace your cellForRowAt indexPath with below:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { var cell : WorldMessageCell cell = tableView.dequeueReusableCell(withIdentifier: "WorldMessageCell", for: indexPath) as! WorldMessageCell if indexPath.row < 14 { cell.messageLabel.text = messages[indexPath.row] } else if indexPath.row >= 14 && indexPath.row != 27 { cell.messageLabel.text = messages[14] } else if indexPath.row == 27 { cell.messageLabel.text = messages.last } return cell }

Ayesha answered 21/1, 2019 at 6:37 Comment(2)
Thank you for your response! Tried what you suggested, but this does not fix the problem :/Morph
I tried it on your test project which you mentioned in the question. It worked, you can try too. In the actual project, there might be different conditions.Ayesha
C
2

According to your answer on my comment that when you set

estimatedHeightForRowAt and heightForRowAt the same values it does work

I can confirm that you are right and that there is the problem that AutoLayout cannot calculate the right value for estimatedHeightForRowAt. So basically there are two possible things to do:

  • find alternative layout that will produce better results
  • make your own calculation for estimatedHeightForRowAt which will produce more accurate results (in general you should be able to tell what is expected height per text length and then add margins to that figure - you need to put a bit of effort to find the proper math, but it should work).
Colorfast answered 21/1, 2019 at 12:48 Comment(2)
Well, it's hard to find alternative layout if it's only 4 constraints (top bottom left right) and 1 label :"D I guess I should make my own calculation that you suggest, but how could I do that? Is there a way to calculate height of a label if it is offscreen, but I know the text & font & width of the label?Morph
Yes, but if that was a good idea then Autolayout would do that already. The point is it is slow and you need something fast. So you make some approximations that are not price but not too far either. You can make some guess on what is the ratio between the character length of the string the and the height of the label. I did something similar and it worked well.Tocopherol
C
2

enter image description here

Just remove highlighted view from UITableView and it's work like a charm.

enter image description here

Hope it helps.

Colossal answered 25/1, 2019 at 5:11 Comment(0)
S
2

This is expected behaviour when using coarse cell height estimates (or not providing them at all, as you do). The actual height is computed only when the cells come on screen, so the travel of the scroll bar is adjusted at that time. Expect jumpy insertion/deletion animations too, if you use them.

Setsukosett answered 25/1, 2019 at 10:49 Comment(0)
W
1

I hope you heard about this a lot. so take short break and come back on desk and apply 2 - 3 steps for step this.

1) Make sure Autolayouts of label of Cell is setup correct like below.

enter image description here

2) UILabel's number of lines set zero for dynamic height of text.

enter image description here

3) setup automatic dimension height of cell.

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

and I believe its should be work. see results of my code.

enter image description here

Wobble answered 25/1, 2019 at 6:38 Comment(0)
A
0

why you add view in table view , it can also work without it. I just delete that view and change some constraints(like bottom constraints change safe area to superview) , and it works fine.

see this video

download storyboard and add it to your project and then check

Antipas answered 21/1, 2019 at 6:49 Comment(1)
I need that view because I place the loading indicator there (in the real app). I’ll try out this when I get home but remember: in my project the problem disappears once the table view scrolled to the bottom for the first time (so after that scrolling up and down works fine). As I see in your video, it starts scrolled down, so maybe the problem is still present. But I’ll let you know! Thank you for your answer!Morph
W
0

Configure your tableview with these in viewDidLoad()

        tableView.estimatedRowHeight = 100.0
        tableView.rowHeight = UITableView.automaticDimension
        tableView.tableFooterView = UIView()

And you should remove both height datasource method.

Whippersnapper answered 21/1, 2019 at 6:52 Comment(2)
This does not solve the problem, but thank you for taking the time to respond!Morph
I tested this on your given test code and seems to work just fine. Not sure what other problem you are facing with this solution.Whippersnapper
N
0

What you want to do is eliminate the extra blank cells. You can do so by setting the tableFooterView to an empty UIView in the viewDidLoad method. I cloned the code from your GitHub and revised the method:

override func viewDidLoad() {
    super.viewDidLoad()
    tableView.register(UINib(nibName: "WorldMessageCell", bundle: nil), forCellReuseIdentifier: "WorldMessageCell")
    tableView.tableFooterView = UIView()
}

setting the tableFooterView to nil worked for me as well

tableView.tableFooterView = nil

enter image description here

Nix answered 22/1, 2019 at 9:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.