How to automatically adapt height of UITableViewCell containing a multiline UIButton? - ios

A subclass of UITableViewCell contains a UIButton with multi-line text, i.e. property numberOfLines = 0.
The table view cells vary in height, so the cell height is set to UITableViewAutomaticDimension.
The cell height adapts when adding a UILabel with multiple text lines. However it does not adapt with a UIButton, in fact also the frame of the button does not adapt to the frame of its titleLabel.
What can I do to make the table view cell and its content view adapt to the button height?
class MyButtonCell: UITableViewCell {
var button: UIButton!
var buttonText: String?
convenience init(buttonText: String?) {
self.init()
self.buttonText = buttonText
button = UIButton(type: UIButtonType.System)
button.titleLabel?.numberOfLines = 0
button.titleLabel?.lineBreakMode = .ByWordWrapping
button.contentHorizontalAlignment = .Center
button.titleLabel?.textAlignment = .Center
button.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubView(button)
NSLayoutConstraint.activateConstraints([
button.topAnchor.constraintEqualToAnchor(self.contentView.topAnchor),
button.bottomAnchor.constraintEqualToAnchor(self.contentView.bottomAnchor),
button.rightAnchor.constraintEqualToAnchor(self.contentView.rightAnchor),
button.leftAnchor.constraintEqualToAnchor(self.contentView.leftAnchor)
])
button.setTitle(buttonText, forState: .Normal)
button.setTitleColor(buttonTextColor, forState: UIControlState.Normal)
button.titleLabel?.font = buttonFont
}
}
The cell height is calculated automatically with:
tableView.estimatedRowHeight = 100
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
UPDATE:
Example project on https://github.com/mtrezza/ButtonCellHeightBug
Filed Apple Bug Report #26170971.
The bug results in this:

Fully dynamic height for table view cell is achievable by 1) using estimated row height, 2) setting rowHeight to AutoDimension, 3) and most importantly using constraints in your xib/storyboard. The cell can contain buttons/labels or whatever UI components you'd like to have, as long as you constrain them properly, particularly to make sure things are constrained vertically so table view can figure out the cell height. And in this way you don't have to calculate height for dynamic text, no need for sizeToFit/sizeThatFit, and it works for different screen sizes.

You should use estimatedHeightForRowAtIndexPath. On your button, you can call sizeToFit(), which will resize it to contain the text.
Also, if you set the estimated size on the tableView (as you did), you usually don't need to call the heightForRowAtIndexPath, or estimatedHeightForRowAtIndexPath, and the tableView will set it for you.
EDIT:
I created a test project, and you seem to be correct. Using a UIButton setTitle does not resize the cell.
A workaround, is to do the calculation using a label in heightForRowAtIndexPath, and return that value + any padding. Then in cellForRowAtIndexPath, you can still set the title on the button and it will appear.
//paragraphs is just a string array.
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let label = UILabel(frame: CGRectMake(0,0,tableView.frame.width, <your prototype height>))
label.numberOfLines = 0
label.text = paragraphs[indexPath.row]
label.sizeToFit()
print(label.frame.height)
return label.frame.height
}
Bug in iOS?

The problem is that the internal UIButtonLabel resizes correctly, but the actual UIButton does not.
I've worked around this by extending UIButton and overriding a couple of things:
override func layoutSubviews() {
super.layoutSubviews()
self.titleLabel?.preferredMaxLayoutWidth = self.titleLabel?.frame.size.width ?? 0
}
override var intrinsicContentSize: CGSize {
return self.titleLabel?.intrinsicContentSize ?? CGSize.zero
}
You'll also need to make sure that titleLabel?.numberOfLines = 0 and titleLabel?.lineBreakMode = .byWordWrapping.

Related

UITableView Alignment with UITextView

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

UITableViewCell wrong content width

i'm using tableview in iOS 9, Swift 2
I don't understand why , when i create a table view with simple cells, i get a wrong width of the contentview, even if i set container view to 1000.
The cell width is exactly 1000, but the textLabel inside is less than 1000. Also separators are centered and with wrong width.
How can i get my cells displayed correctly , and covering the entire container view?
Everything is created programmatically.
Here is my code:
if tableView == nil {
tableView = UITableView(frame: CGRect(x: 0, y: 44/*88*/, width: self.view.bounds.size.width, height: self.view.bounds.size.height - 44/*88*/), style: UITableViewStyle.Plain)
tableView!.delegate = self
tableView!.backgroundColor = .clearColor()
tableView!.dataSource = self
tableView!.rowHeight = 40.0
tableView!.allowsSelection = false
tableView!.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
view.addSubview(tableView!)
tableView!.reloadData()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var MyIdentifier: String = "MyReuseIdentifier"
var cell: UITableViewCell? = tableView.dequeueReusableCellWithIdentifier(MyIdentifier)
if cell == nil {
cell = UITableViewCell(style: .Default, reuseIdentifier: MyIdentifier)
}
cell!.textLabel!.text = "Test"
cell!.backgroundColor = .clearColor()
cell!.textLabel!.textColor = .blackColor()
return cell!
}
tableView!.cellLayoutMarginsFollowReadableWidth = false
This is the solution!
It is not necessary to set contentInsets.
While the cells's width is the width of the entire view, it's content view is inset on all sides by (I think) 8 points, so it makes sense for the label not to be across the whole screen
To have a label that goes across the entire width of the screen create a custom cell class and add constraints to its label to account for this. (Label's leading to content view leading = -8.0)
You can check
tableView.contentInset
And then update left inset. Also, check that you have not given any constraint, like leadingSpace. Or margin to the UITableView itself. Similarly you have
cell.separatorInset
By default there is left padding of 5, so make this 0.

Changing the height of a dynamic-height UITableViewCell after it's already appeared

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
}

sizeToFit() returns wrong height - Need to find cell width in heightForRow

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
(&ast;) 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.

Expand UITableViewCell when size of contents changes

I have a UITextView inside a static UITableViewCell constrained like this:
Picture
In the viewDidLoad() method of my table view class I want to be able to change the text of the UITextView, have the UITextView change size to fit the text (I have scrolling disabled on the UITextView), and then have the UITableViewCell still be constrained as I have intended it to be. This is my attempt to do so:
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.aboutTextView.text = "Some Long String"
//implement self sizing cells
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 400.0
//set the frame of the UITextView to match the size of the text
let fixedWidth = aboutTextView.frame.size.width
let newSize = aboutTextView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.max))
var newFrame = aboutTextView.frame
newFrame.size = CGSize(width: max(newSize.width, fixedWidth), height: newSize.height)
aboutTextView.frame = newFrame;
//This is returning the new size correctly
//Reload the tableview, nothing happens, text view remains the same size as in the storyboard
self.tableView.reloadData()
}
Although the new frame is bigger than the default size in the storyboard, nothing happens when the tableView is reloaded. I have tried setNeedsLayout() on the UITextView with no luck as well. I also tried constraining the height of the UITextView and changing the constant of the height through an IBOutlet but then the constraints break for obvious reasons. Anybody know why my code isn't working? Any better method to do what I am trying to do?
All the code about the row height should go in your tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) function. That way, whenever the table is reloaded, the code will be executed again, changing the table height.
Edit: I completely misunderstood what you were doing. In the storyboard, try adding constraints to the UITextView within the cell and give it a constraint on the height, and then make sure that the cell also has a height constraint slightly larger than that of the UITextView.

Resources