iOS UITableView unable to compute proper cell height for each cell - ios

The basic setup is like this: I have a UITableView with two proto cells that I am working with (think of a messaging app, where one cell type shows messages you send and the other, messages you recieve). Now obviously the message length can vary from one line to even 100+ lines thus I need variable cell heights.
First attempt:
tableView.estimatedRowHeight = 75
tableView.rowHeight = UITableViewAutomaticDimension
I used estimatedRowHeight in my viewDidLoad(). This works perfectly and computes cell heights very nicely. But because this is a messaging app I need to scroll the tableView all the way to bottom on viewDidLoad() and whenever a new message is recieved / sent. But the estimatedRowHeight messes with the tableView scrolling all the way to bottom. Some say it's a bug, some say it's to be expected. Nonetheless, this way won't work for me at all.
Second Attempt:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
I thought to compute heights manually as so:
let measuerWidth = UIScreen.mainScreen().bounds
var w = measuerWidth.size.width // this is so that we can limit label width to screen width so the text is forced to go to multiple lines
// I probably should use my custom cell width here, but if I try to `dequeue` that cell it's frame contents are always `zero`. Is that a problem?
let lbl = UILabel(frame: CGRect.zeroRect)
lbl.text = chatMessages[indexPath.row][ChatRoomKeys.MESSAGE_TEXT] as? String
lbl.font = UIFont.systemFontOfSize(20)
lbl.numberOfLines = 0
lbl.lineBreakMode = NSLineBreakMode.ByWordWrapping
lbl.frame = CGRectMake(0, 0, w, 0)
lbl.sizeToFit()
return lbl.frame.height + 20
This way works almost perfectly however at times cell height isn't what it should be. Meaning sometimes if the text is one line plus one word, that one word won't show because the cell height was only for one line.
Is there some better way to calculate the cell height?
UPDATE:
Here's a screenshot of kinda what happens
as you can see the label ends at x but the original text goes upto y.
UPDATE 2:
These are the proto cells I am using, the bottom cell is simply a mirror of the top one.

I use this one:
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 2)
{
NSString *cellText = #"init some text";
CGSize labelSize = [self calculateTextSize:cellText];
return labelSize.height + 20.0f;
}
return 45;
}
- (CGSize) calculateTextSize: (NSString*) text
{
UIFont *cellFont = [UIFont fontWithName:#"HelveticaNeue" size:12.0];
CGFloat width = CGRectGetWidth(self.tableView.frame) - 40.0f;
CGSize constraintSize = CGSizeMake(width, MAXFLOAT);
CGRect labelRect = [cellText boundingRectWithSize:constraintSize options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName:cellFont} context:NSLineBreakByWordWrapping];
CGSize labelSize = labelRect.size;
return labelSize;
}
Edit:
CGFloat width = CGRectGetWidth(self.tableView.frame) - 40.0f;
Calculation of the maximum available width of the text, you can use self.view.frame or etc. -40.0f - because i have indentation from the edge = 20.0f

You can do this with the automatic height as you were doing. See the code below, when your view is about to appear, ask the UITableView for the number of rows in the section (0) in this case. Then you can create an NSIndexPath for the last row in that section then ask the UITableView to scroll that index path into view. Using the code below you can still have the UITableView calculate the heights for you.
import UIKit
final class ViewController: UIViewController {
// MARK: - Constants
let kFromCellIdentifier = "FromCell"
let kToCellIdentifier = "ToCell"
// MARK: - IBOutlets
#IBOutlet private weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 75.0
tableView.rowHeight = UITableViewAutomaticDimension
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let numRows = tableView.numberOfRowsInSection(0) - 1 // -1 because numbering in the array starts from 0 not 1
let indexPath = NSIndexPath(forRow: numRows, inSection: 0)
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: false)
}
}
extension ViewController: UITableViewDataSource {
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row % 2 == 0 {
let cell = tableView.dequeueReusableCellWithIdentifier(kFromCellIdentifier, forIndexPath: indexPath) as! CustomTableViewCell
cell.configureCell(UIImage(named: "Talk")!, text: "From Some text \(indexPath.row)")
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier(kToCellIdentifier, forIndexPath: indexPath) as! CustomTableViewCell
cell.configureCell(UIImage(named: "Email")!, text: "To Some text \(indexPath.row)")
return cell
}
}
}

Related

Dynamically change UITableViewCell height - Swift 4

How do I go about dynamically changing the UITableViewCell height? I've tried implementing the following, but for some reason, it isn't working. The code crashes as soon as I load the view controller displaying this table view
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let cell = tableView.cellForRow(at: indexPath) as! AvailableRideCell
return cell.getHeight()
}
This is the getHeight function in my AvailableRideCell
func getHeight() -> CGFloat {
return self.destinationLabel.optimalHeight + 8 + self.originLabel.optimalHeight + 8 + self.priceLabel.optimalHeight
}
And this is the optimalHeight function
extension UILabel {
var optimalHeight : CGFloat {
get {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = self.font
label.text = self.text
label.sizeToFit()
return label.frame.height
}
}
}
Keep in mind that UITableViewCell is reused. So getting the height of the current cell can be unstable.
A better way is to have one fake/placeholder cell (I call the calculator cell) and use that to calculate the size of the cell.
So in the heightForRowAt method, you get the data instead of the cell.
Put that data inside the calculator cell and get the height from there.
You code crashes because of this line
let cell = tableView.cellForRow(at: indexPath) as! AvailableRideCell
From Apple Documentation we know that this method is used for optimization. The whole idea is to get cells heights without wasting time to create the cells itself. So this method called before initializing any cell, and tableView.cellForRow(at: indexPath) returns nil. Because there are no any cells yet. But you're making force unwrapping with as! AvailableRideCell and your code crashed.
So at first, you need to understand, why you should not use any cells inside the cellForRow(at ) method.
After that, you need to change the logic so you could compute content height without calling a cell.
For example, in my projects, I've used this solution
String implementation
extension String {
func height(for width: CGFloat, font: UIFont) -> CGFloat {
let maxSize = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
let actualSize = self.boundingRect(with: maxSize,
options: [.usesLineFragmentOrigin],
attributes: [NSAttributedStringKey.font: font],
context: nil)
return actualSize.height
}
}
UILabel implementation
extension String {
func height(for width: CGFloat, font: UIFont) -> CGFloat {
let labelFrame = CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude)
let label = UILabel(frame: labelFrame)
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.font = font
label.text = self
label.sizeToFit()
return label.frame.height
}
}
With that, all you need to do is to compute your label and store its font.
var titles = ["dog", "cat", "cow"]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// number of rows is equal to the count of elements in array
return titles.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let cellTitle = titles[indexPath.row]
return cellTitle.height(forWidth: labelWidth, font: labelFont)
}
Dynamic rows height changing
If you'll need to update row height, all you need to do is to call this method when your content had been changed. indexPath is the index path of changed item.
tableView.reloadRows(at: [indexPath], with: .automatic)
Hope it helps you.
You don't mention if you are using Auto Layout, but if you are, you can let Auto Layout manage the height of each row. You don't need to implement heightForRow, instead set:
tableView.rowHeight = UITableViewAutomaticDimension
And configure your UILabel with constraints that pin it to the cell's content view:
let margins = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
cellLabel.leadingAnchor.constraint(equalTo: margins.leadingAnchor),
cellLabel.trailingAnchor.constraint(equalTo: margins.trailingAnchor),
cellLabel.topAnchor.constraint(equalTo: margins.topAnchor),
cellLabel.bottomAnchor.constraint(equalTo: margins.bottomAnchor)
])
Each row will expand or contract to fit the label's intrinsic content size. The height is automatically adjusted if the device landscape/portrait orientation changes, without re-loading the cell. If you want the row height to change automatically when the device's UIContentSizeCategory changes, set the following:
cellLabel.adjustsFontForContentSizeCategory = true

increase TableView height based on cells

I need to increase UITableView height based on UITableViewCell content size.
I'm using Custom Google Auto Complete. I have an UITextField. When I enter a letter in that UITextField it will call shouldChangeCharactersIn range delegate method.
Inside that method I will send dynamic request to Google AutoComplete API to get predictions result.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if tableView.numberOfRows(inSection: 0) > 0
{
let newLength = (textField.text?.characters.count)! + string.characters.count - range.length
let enteredString = (textField.text! as NSString).replacingCharacters(in: range, with:string)
if newLength == 0 {
tableView.isHidden = true
}
if newLength > 0
{
address.removeAll()
self.tableView.isHidden = false
GetMethod("https://maps.googleapis.com/maps/api/place/autocomplete/json?input=\(enteredString)&key=MYAPIKEY", completion: { (resultDict) in
let resultArray = resultDict.object(forKey: "predictions")! as! NSArray
print(resultArray.count)
for temp in resultArray
{
let newtemp = temp as! NSDictionary
let tempAdd = newtemp.value(forKey:"description") as! String
let placeId = newtemp.value(forKey:"place_id") as! String
var dict = [String : AnyObject]()
dict["address"] = tempAdd as AnyObject?
dict["latlong"] = placeId as AnyObject?
self.address.append(dict as AnyObject)
print(newtemp.value(forKey:"description"))
print(self.address.count)
self.tableView.reloadData()
}
})
}
return true
}
After I will store all address to Address array dynamically, I need to increase UITableView height based on that incoming address content.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "TableViewCell"
var cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
if cell == nil
{
cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier)
}
let addresstoDisp = address[indexPath.row] as! NSDictionary
let name = addresstoDisp.value(forKey: "address")
cell?.textLabel?.numberOfLines = 0
cell?.translatesAutoresizingMaskIntoConstraints = false
cell?.textLabel?.lineBreakMode = NSLineBreakMode.byWordWrapping
cell?.textLabel?.textAlignment = NSTextAlignment.center
cell?.textLabel?.text = name as! String
return cell!
}
UITableViewCell height is increasing perfectly. Also I need to increase tableView height.
Add these lines after your cells are created. Because it returns 0 height in viewDidLoad()
var frame = tableView.frame
frame.size.height = tableView.contentSize.height
tableView.frame = frame
UPDATE
//After Tableviews data source values are assigned
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.heightAnchor.constraint(equalToConstant: tableView.contentSize.height).isActive = true
The below code worked for me with UITableViewCell who has AutomaticDimension.
Create an outlet for tableViewHeight.
#IBOutlet weak var tableViewHeight: NSLayoutConstraint!
var tableViewHeight: CGFloat = 0 // Dynamically calcualted height of TableView.
For the dynamic height of the cell, I used the below code:
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return tableView.estimatedRowHeight
}
For height of the TableView, with dynamic heights of the TableView Cells:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
print(cell.frame.size.height, self.tableViewHeight)
self.tableViewHeight += cell.frame.size.height
tableViewBillsHeight.constant = self.tableViewHeight
}
Explanation:
After the TableView cell is created, we fetch the frame height of the cell that is about to Display and add the height of the cell to the main TableView.
In your viewDidLoad write:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
myTable.reloadData()
myTable.layoutIfNeeded()
}
Now override the viewDidLayoutSubviews method to give the tableview explicit height constraint:
override func viewDidLayoutSubviews() {
myTable.heightAnchor.constraint(equalToConstant:
myTable.contentSize.height).isActive = true
}
This makes sure that the tableview is loaded and any constraints related layout adjustments are done.
Without setting and resetting a height constraint you can resize a table view based on its content like so:
class DynamicHeightTableView: UITableView {
override open var intrinsicContentSize: CGSize {
return contentSize
}
}
I have been struggling with scroll view that must increase height when table view cells are loaded and also table view shouldn't be scrollable as it should display all cells (scroll is handled by scroll view). Anyway, you should use KeyValueObserver. First you create outlet for height constraint:
#IBOutlet weak var tableViewHeightConstraint: NSLayoutConstraint!
Then you add observation for table view:
private var tableViewKVO: NSKeyValueObservation?
After that, just add table view to observation and change height constraint size as your content size changes.
self.tableViewKVO = tableView.observe(\.contentSize, changeHandler: { [weak self] (tableView, _) in
self?.tableViewHeightConstraint.constant = tableView.contentSize.height
})
this is what works for me, very simple straight forward solution:
Create a new UIElement of the TableView height constraint and connecting it to the view
#IBOutlet weak var tableViewHeightConst: NSLayoutConstraint!
Then add the following wherever you are creating your cells, in my case I was using RxSwift
if self.tableView.numberOfRows(inSection: 0) > 0 {
//gets total number of rows
let rows = self.tableView.numberOfRows(inSection: 0)
//Get cell desired height
var cellHeight = 60
self.tableViewHeightConst.constant = CGFloat(rows * cellHeight)
}
this should do the trick.

UITableView Dynamic Row Height Without Using AutoLayout

I am supporting iOS 7 and I am not using autolayout. Is there a way I can have dynamic height for cell labels doing it this way?
Thanks
EDIT:
Here is the code I am using to define a dynamic height in iOS 7, it seems I can get it kinda working with auto layout but it cuts off the last cell at the bottom, it is weird.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
var cell = offScreenCells.objectForKey("gcc") as? GalleryCommentCell
if cell == nil {
cell = commentsTable.dequeueReusableCellWithIdentifier("GalleryCommentCustomCell") as? GalleryCommentCell
offScreenCells.setObject(cell!, forKey: "gcc")
}
let comment :GalleryCommentInfo = commentResults[indexPath.row]
setCellCommentInfo(cell!, data: comment)
cell!.bounds = CGRectMake(0, 0, CGRectGetWidth(commentsTable.bounds), CGRectGetHeight(cell!.bounds))
cell!.setNeedsLayout()
cell!.layoutIfNeeded()
let height = cell!.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
return height + 1
}
func setCellCommentInfo(cell :GalleryCommentCell, data :GalleryCommentInfo) {
cell.commentDate.text = data.galleryCommentDate
cell.comment.text = data.galleryComment
}
In your custom cell implement method like this:
+ (CGFloat)heightForContactName:(NSString *)name
{
CGFloat height = 0.0f;
if (name) {
CGFloat heightForText;
// Calculate text height with `textBoundingRect`...
height += heightForText;
}
return height;
}

How to get UITableViewCell to fit entire TextView, regardless of size of text?

I am attempting to display a text field in each table cell. The problem is I cannot seem to get the UITableViewCell to be tall enough to fit the whole TextView. The UITableViewCell does not need to be dynamic, as the data is loaded into the UITextView on the view's load.Here is my code:
//
// chatViewController.swift
// collaboration
//
// Created by nick on 11/21/15.
// Copyright © 2015 Supreme Leader. All rights reserved.
//
import UIKit
class chatViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var testLabel: UILabel!
var messages: NSMutableArray = NSMutableArray()
var authors: NSMutableArray = NSMutableArray()
func tableView(tableView: UITableView, numberOfRowsInSection Section: Int) -> Int {
return self.authors.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! chatTableViewCell
cell.authorLabel?.text = self.authors.objectAtIndex(indexPath.row) as? String
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
let receivedUsername = NSUserDefaults.standardUserDefaults().stringForKey("usernameToMessageWith")
testLabel.text = "\(receivedUsername! as String)"
authors.addObject("")
authors.addObject("You:")
CGRect frame = messagesTextView.frame;
frame.size = messagesTextView.contentSize;
messagesTextView.frame = frame;
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
I have absolutely no idea how to go about doing this. Please do not make fun of me for my pathetic screwed up scope attempt.
If you are using a textField you have to calculate the height programatically and set it when displaying your cell. If it's not editable however you can do the following with a UILabel.
In you viewDidLoad set tableView.estimatedRowHeight = <expected height> and tableView.rowHeight = UITableViewAutomaticDimension
In your custom cell you then need to set layout constraint for the top and bottom of the UILabel towards the edges of the contentView and set UILabels lines to 0
Possible workaround for textfield:
Use both a textfield and a UILabel and set the UILabels text color to clear color. This way you can use the UILabel to adjust the height but still have the functionality of the text field also.
The code
tableView.estimatedRowHeight = <expected height>
tableView.rowHeight = UITableViewAutomaticDimension
will only work for iOS > = 8
If you want to make the code compatible for iOS 7.x too you can try the code below.
As suggested instead of textview or textfield use UILabel if text is not editable. Here is a workaround to calculate the height with UILabel.
Provide the name of the font which you are using and the size of the font used exactly the same. which you have kept.
func requiredHeight(text:String) -> CGFloat{
let font = UIFont(name: "Helvetica", size: 16.0)
let label:UILabel = UILabel(frame: CGRectMake(0, 0, 200, CGFloat.max))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.ByWordWrapping
label.font = font
label.text = text
label.sizeToFit()
return label.frame.height + //Add some space as a part of your bottom and top constraint
}
Suppose your label has top and bottom constraint as 10 and 10 respectively, them make the return statement as:
return label.frame.height + 20
Call this method in heightForRowAtIndex and pass the text there as a parameter. It will return a dynamic height based on the calculations.
EDITHow to use
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return requiredHeight("Your text")
}

Single line text takes two lines in UILabel

As you can see in the picture the middle cell has a UILabel that consumes two lines, but the text is actually a single line. It seems that if the text needs only few characters to create a new line, iOS assumes that it already has 2 lines. This is odd.
This is how I create the label:
self.titleLabel.lineBreakMode = .ByTruncatingTail
self.titleLabel.numberOfLines = 0
self.titleLabel.textAlignment = .Left
The constraints are set once:
self.titleLabel.autoPinEdgeToSuperviewEdge(.Top)
self.titleLabel.autoPinEdgeToSuperviewEdge(.Leading)
self.titleLabel.autoPinEdgeToSuperviewEdge(.Trailing)
self.titleLabel.autoPinEdgeToSuperviewEdge(.Bottom)
The weird thing is, when the table is scrolled so that the odd cell disappears and scrolled back again it has its normal height. After scroll:
Any ideas whats wrong? I am using swift, xcode6.1 and iOS8.1
TableViewController:
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.registerClass(CityTableViewCell.self, forCellReuseIdentifier:"cell")
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 52
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if let cell: CityTableViewCell = tableView.dequeueReusableCellWithIdentifier("cell") as? CityTableViewCell {
// Configure the cell for this indexPath
cell.updateFonts()
cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator
if indexPath.row == 1 {
cell.titleLabel.text = "Welcome to city 17, Mr. Gordon F."
} else {
cell.titleLabel.text = "Lamar!!!"
}
// Make sure the constraints have been added to this cell, since it may have just been created from scratch
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
return cell
}
return UITableViewCell();
}
I think you met this bug: http://openradar.appspot.com/17799811. The label does not set the preferredMaxLayoutWidth correctly.
The workaround I chose was to subclass UITableViewCell with the following class:
class VFTableViewCell : UITableViewCell {
#IBOutlet weak var testoLbl: UILabel!
//MARK: codice temporaneo per bug http://openradar.appspot.com/17799811
func maxWidth() -> CGFloat {
var appMax = CGRectGetWidth(UIApplication.sharedApplication().keyWindow.frame)
appMax -= 12 + 12 // borders, this is up to you (and should not be hardcoded here)
return appMax
}
override func awakeFromNib() {
super.awakeFromNib()
// MARK: Required for self-sizing cells.
self.testoLbl.preferredMaxLayoutWidth = maxWidth()
}
override func layoutSubviews() {
super.layoutSubviews()
// MARK: Required for self-sizing cells
self.testoLbl.preferredMaxLayoutWidth = maxWidth()
}
}
OP-Note:
It seems that auto layout does not calculate the width of the UILabel layout correctly. Setting the preferred width to the parents width in my UITableViewCell subclass solves my problem:
self.titleLabel.preferredMaxLayoutWidth = self.frame.width
Found on SO: https://stackoverflow.com/a/19777242/401025

Resources