I have a UITableView with custom UITableViewCells. Each cell has a bunch of UIView, UIStackView, and UILabel subviews to build out the design I want.
Everything within the UITableViewCell is positioned using AutoLayout, and I allow the UITableView to automatically give height to the cells by doing:
tableView.estimatedRowHeight = 85
tableView.rowHeight = UITableViewAutomaticDimension
However, because of the complexity of the cell's subview layout, scrolling down the tableView is very laggy/choppy as each cell tries to estimate and build out it's correct height.
I read up on some articles that discuss these issues like:
https://medium.com/ios-os-x-development/perfect-smooth-scrolling-in-uitableviews-fd609d5275a5
This inspired me to try a different approach where I would calculate the height of each cell by adding up all the vertical space, and calculating dynamic string height by utilizing functions like the following, and then caching these heights in a dictionary of [IndexPath.row (Int), Row Height (CGFloat)].
extension String {
func heightWithConstrainedWidth(width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: CGFloat.max)
let boundingBox = self.boundingRectWithSize(constraintRect, options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return boundingBox.height
}
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if let height = cellHeightCache[indexPath.row] {
return height
} else {
let data = dataItems[indexPath.row]
var height: CGFloat = 0
height += 20 // top padding above text
height += data.title.heightWithConstrainedWidth(tableView.frame.width - 72 * 2, font: UIFont.boldSystemFontOfSize(18)) // height of title
height += 20 // bottom padding below text
... // this goes on and on with if statements depending on if the data has a subtitle, image, other properties, etc to get the correct height for *that* cell
cellHeightCache[indexPath.row] = ceil(height)
return height
}
}
While this does work, now I am mixing frame calculations, tableView.frame.width - 72 * 2 with the AutoLayout of the cell's subviews to get the height. And because these are frame calculations, if the user rotates the device or on an iPad brings over the right split screen view, this clearly breaks. I could try and catch all these edge cases and recalculate when the user rotates/changes the size of the screen, but am I missing a way to calculate the height of these cells without using frames?
Is there anyway just using AutoLayout positioning I can calculate the height of the cells in the heightForRowAtIndexPath function?
Thanks
Related
I am trying to make a UITableView line up with the height sizing of paragraphs in a UITextView. Example: The timestamps to the left are what I am trying to do. I changed my code to use UIView's instead of TVcells to see what was wrong and you can see the orange view is overlapping the cyan one, meaning that the views don't actually line up but they overlap. NOTE: I am wanting to use the TableView not UIView's I am having trouble understanding how the text heights are calculated in iOS. I am using the below code to get the heights of each paragraph:
let liveParagraphView = textView.selectionRects(for: txtRange).reduce(CGRect.null) { $0.union($1.rect) }
After this I calculate the height of each then feed that into my UITableView heightForRowAt
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let models = getParagraphModel()
let height = models[indexPath.row].height
let finalHeight = models[indexPath.row].height
let heightValue = finalHeight
return CGFloat(heightValue);
}
Every line has different height values but even when using these values it's not lining up. The problem seems to be that every line calculates a Y Position which is not directly under the line before it. It's ON TOP OF!! Resulting in the UITableView not being alined when new cells are added and that 'overlay' of the selectionRects isn't taken into account. Am I correct by this? How could I go about achieving this?
Swift 5
Firstly you should set your textView (which is in the cell) dynamic height:
textView.translatesAutoresizingMaskIntoConstraints = true
textView.sizeToFit()
textView.isScrollEnabled = false
Then calculate your textView's number of lines in textDidChange etc. for update tableView's layout.
let numOfLines = (yourTextView.contentSize.height / yourTextView.font.lineHeight) as? Int
When textView's text one line down you should update tableView layout:
tableView.beginUpdates()
tableView.endUpdates()
And then you should set your tableView cell's intrinsicContentSize for dynamic rowHeight:
Set your cell's (which is the contains textView) layout without static height,
Set your tableView's rowHeight and estimatedRowHeight:
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 44 // whatever you want
So now you have tableView cell with dynamicHeight
In my story board I have a UITableView with dynamically generated UITableViewCells. Each cell contains 2 labels and 1 text view:
I have a code that adjust the size of the textfield to the amount of text:
let fixedWidth = cell.myComment.frame.size.width
cell.myComment.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.max))
let newSize = cell.myComment.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.max))
var newFrame = cell.myComment.frame
newFrame.size = CGSize(width: max(newSize.width, fixedWidth), height: newSize.height)
cell.myComment.frame = newFrame;
and it works fine, when I set a background color of my textView to red I see:
and the cell itself - I'm setting the size in here:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
if indexPath.row == 0 {
return 100//this is my static cell
}
else {
return 117; //for now it's hardcoded - how can I set this value dynamically based on content?
}
}
So as I wrote in the comment above - how can I set the height of the cell dynamically based on the amount of text in the text view?
The key to getting self-sizing table cells (autolayout-based, which I recommend) is as follows:
Add your subviews to the contentView of the UITableViewCell
Provide constraints between your subviews and the contentView such that your subviews reach all edges of the table cell. In your case, this probably means aligning the leading, trailing, top, and bottom edges of your UITextView to the corresponding edges of the contentView.
Set the row height to UITableViewAutomaticDimension instead of a hardcoded CGFloat.
Somewhere in your controller, provide an estimation of the height with tableView.estimatedRowHeight = x (a hard coded constant is fine, this is for performance).
I followed this tutorial to create a dynamic height UITableViewCell.
My prototype cell has 1 label (pretty simple). That label has constraints set to "0" for all sides of the cell edges. That label has numberOfLines = 0 and wraps the word.
In my UITableView setup, I do this:
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 100.0 //some random number
Basically, everything works. Each cell has its own different height.
The problem arises when I update that label with different text. The height of the cell stays the same, and the text just gets truncated OR white space appears.
Is there a way to tell the cell to "redraw" itself so that it calculates the height again?
Ideally, I'd like to do this inside the UITableViewCell class, since that's where it receives a notification to update the label text.
You have to implements estimatedHeightForRowAtIndexPath and calculate your label's size.
for example :
override func tableView(tableView: UITableView,
estimatedHeightForRowAtIndexPath
indexPath: NSIndexPath) -> CGFloat {
let model = YourModelArray[indexPath.row]
// Label fontName and size
let attributes = [
NSFontAttributeName : UIFont(name: "FontName-Regular", size: 16) as! AnyObject]
let nsStringText = model.text as! NSString
// Calculate the size here
//--------------------//
//--10--UILabel--10--//
//-----Element------//
//------------------//
let size = nsStringText.
boundingRectWithSize(CGSize(width: self.tableView.frame.width - 20,
height: 1000),
options:[NSStringDrawingOptions.UsesLineFragmentOrigin,
NSStringDrawingOptions.UsesFontLeading],
attributes: attributes, context: nil)
// Basically add other elements heights and vertical Spacing between them
return size.height + element.frame.height + verticalSpacingBetweenLabelAndElement
}
I am trying to make a UITableView with dynamic cell heights, based on the content. My app is an article viewer. I try to calculate the height of the cells with these functions.
func heightForView(text:String, font:UIFont, width:CGFloat) -> CGFloat{
let label:UILabel = UILabel(frame: CGRectMake(0, 0, width, CGFloat.max))
label.numberOfLines = 0
label.lineBreakMode = .ByTruncatingTail
label.font = font
label.text = text
label.sizeToFit()
return label.frame.height
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let post = FeedController.sharedController.allPosts[indexPath.row]
var height = 16.0
if post.hasImage {
height += Double(self.view.frame.width) / (4/3)
}
let titleH = heightForView(post.title!, font: UIFont(name: "LiberationSerif-Bold", size: 20.0)!, width: self.tableView.bounds.width)
let descH = heightForView(post.desc!, font: UIFont(name: "LiberationSerif", size: 17.0)!, width: self.tableView.bounds.width)
let totalHeight = CGFloat(height+Double(titleH)+Double(descH))
print("imageH: \(height), titleH: \(titleH) detailH: \(descH) totalH:\(totalHeight)")
return totalHeight
}
When I run it, it looks like this, as you can see, the bottom label is cut off.
I ran it trough the interface breakpoint thingy, where you can see the cause of the error:
The rendered size is one size larger than the estimated size from sizeToFit(), as you can see from my log statement:
imageH: 297.25, titleH: 44.5 detailH: 56.5 totalH:398.25
I suspect the reason for this calculation error is due to me providing the heightForView function with the wrong width parameter. I have tried to use tableView.contentSize, and many other things. The lineBreakMode is the same in my storyboard and in my code.
I guess what I need is a better way to find the expected width of the cell, or any other solution which will give me the proper expected height.
Edit: Constraints:
Automatic Dimension
With much less code(*), you can ditch heightForRowAtIndexPath entirely, and use autolayout for everything.
Do not overwrite heightForRowAtIndexPath (all that code goes away)
Tell the table view to do the hard work (in viewDidLoad for example):
self.tableView.estimatedRowHeight = 88
self.tableView.rowHeight = UITableViewAutomaticDimension
(*) 2 lines of code, to be precise.
Convince yourself by comparing 2 lines solution against multiple methods override solution.
Notes on margin
XIB & storyboard: you have the ability to control the constraints and their relationships to margin.
Note that when using Relative to margin, the item order matters, as margins are generally insets.
See https://stackoverflow.com/a/25420909/218152 for some details.
storyboard: you have access to the additional topLayoutGuide and bottomLayoutGuide properties from IB, which may be of value for your view controller, but are off-topic for your UITableViewCell.
I had taken two view inside content view.
Height of "Post Container View" (Red background) is calculated dynamically as per height of label (All done using Autolayout).
Now I want that if height of "Post Container View" (Red background) will increase then height of cell view auto increase. I want to do this using autolayout.
I want to calculate height of UITableview cell using Autolayout. How to do it ?
Cell Height = Post Container View (Flexible as per label height)+ Image Container View Height (300 Fix)
I had seen this type of method, but dont know how to implement in my code ?
- (CGFloat)calculateHeightForConfiguredSizingCell:(UITableViewCell *)sizingCell
{
sizingCell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(self.MyTableView.frame), CGRectGetHeight(sizingCell.bounds));
[sizingCell setNeedsLayout];
[sizingCell layoutIfNeeded];
CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return size.height + 1.0f; // Add 1.0f for the cell separator height
}
To calculate height of cell :
CGFloat height ; // take global variable
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
height = cell.PostContainerView.frame.size.height ;
}
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath
{
height = height + 300 //(300 is Fix height of Image Container View ) ;
return height ;
}
Do you want to calculate the height, or the tableView to calculate the height itself, considering your autolayout configuration ?
To do so, don't implement the delegate method heightForRowAtIndexPath but estimatedHeightForRowAtIndexPath instead (with whatever value you want for the moment).
The tableView will determine the height of the cell considering autolayout constraints applied on it.
Be warned that sometimes you need to call layoutIfNeeded on your cell, after you updated it. (for exemple in the cellForRowAtIndexPath)
Use this Extension class for string:
import UIKit
extension String {
func sizeOfString (font: UIFont, constrainedToWidth width: Double) -> CGSize {
return NSString(string: self).boundingRectWithSize(CGSize(width: width, height: DBL_MAX),
options: NSStringDrawingOptions.UsesLineFragmentOrigin,
attributes: [NSFontAttributeName: font],
context: nil).size
} }
In UITableView delegate calculate UILabel width:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let item = "hkxzghkfjkgjkfxkj jjhghdjajfjkshgjkhkkn jhkhhgdfgjkhsfjkdghhhsxzgnfshgfhk jhsfgfhjfhghj "
let widthOfLabel = Double(view.frame.size.width) - 30
let textHeight = item.sizeOfString(UIFont.systemFont(14), constrainedToWidth: widthOfLabel)
return (padding + textHeight.height)
}