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
Related
I am creating a chat feature in my app, but the cells are getting cut off. I've reviewed the Ray Wenderlich tutorial and the SO questions, but I think my problem is that I combine code with auto layout and I'm missing something (for instance, I don't put a label for the message text on the storyboard, but I do put the imageView). Here's what it looks like when I run it:
Here is the code for the tableView setup:
In viewDidLoad:
tableView.estimatedRowHeight = 600
tableView.rowHeight = UITableView.automaticDimension
Other code:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Get a preconfigured messageCell
let cell = tableView.dequeueReusableCell(withIdentifier: Constants.Storyboard.messageCell) as! MessageCell
// Send the message to it for set up.
cell.setMessage(message: messages[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
And here is the setMessage function in the custom cell:
#IBOutlet var messageImage: UIImageView!
func setMessage(message: Message) {
if message.who == "me" {
// User wrote this message, show it on the right in blue as an outgoing message.
let text = message.txt
let label = UILabel()
label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 18)
label.textColor = .white
label.text = text
let constrainRect = CGSize(width: 0.66 * self.frame.width,
height: .greatestFiniteMagnitude)
let boundingBox = text.boundingRect(with: constrainRect, options: .usesLineFragmentOrigin, attributes: [.font: label.font!], context: nil)
label.frame.size = CGSize(width: ceil(boundingBox.width), height: ceil(boundingBox.height))
let bubbleImageSize = CGSize(width: label.frame.width + 28, height: label.frame.height + 20)
messageImage = UIImageView(frame: CGRect(
x: self.frame.width - bubbleImageSize.width - 20,
y: self.frame.height - (bubbleImageSize.height),
width: bubbleImageSize.width,
height: bubbleImageSize.height))
let bubbleImage = UIImage(named: "outgoing-message-bubble")?
.resizableImage(withCapInsets: UIEdgeInsets(top: 17, left: 21, bottom: 17, right: 21), resizingMode: .stretch)
.withRenderingMode(UIImage.RenderingMode.alwaysTemplate)
messageImage.image = bubbleImage
self.addSubview(messageImage)
label.center = messageImage.center
self.addSubview(label)
} else {
// Other person wrote this message. Show it on the left in gray.
}
}
There are no constraints on messageImage, but I have tried it with constraints of zero around it and the result is the same. I've run the code with breaks and confirmed the sizes are all accurate for the text and the bubble image. But the content view of the cell is cutting off the height to a standard cell size. I've worked on this for over a day. Any help would be appreciated.
I have a problem with some TextField inside a UITableViewCell; i got two textfield inside of the table cell, when I tap on the the textField everything works fine as you can see in this two screenshots
1.
The number in red squares are my UITextField, when I tap on one of them it works fine
But when i click on the other textField the entire cell disappear like this
I have an empty space after the click on the other textfield
I have no function implemented, only a function that change font and textColor
func setPickers() {
self.hourPicker.delegate = self
self.minutePicker.delegate = self
hourPicker.textColor = theme.grey
minutePicker.textColor = theme.grey
hourPicker.background = UIImage()
minutePicker.background = UIImage()
hourPicker.textAlignment = .center
minutePicker.textAlignment = .center
hourPicker.font = UIFont(name: "Roboto-Regular", size: 48)
minutePicker.font = UIFont(name: "Roboto-Regular", size: 48)
}
This the cell in my storyboard
EDIT 2
Look my graphic debug what shows before the bug
And after it
The cell is called EventDetailFooterTableViewCell
EDIT 3
Here is where I initialize the cellView for the footer
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "footerCell") as! EventDetailFooterTableViewCell
cell.event = self.event
cell.delegate = self
cell.setView()
cell.backgroundColor = theme.mainColor
return cell
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
var height: CGFloat = 180.0
var calendar = NSCalendar.current
calendar.timeZone = TimeZone(abbreviation: "UTC")! //OR NSTimeZone.localTimeZone()
let dateAtMidnight = calendar.startOfDay(for: Date())
let todayLong = dateAtMidnight.millisecondsSince1970
if let eventDay = event.dateTime?.millisecondsSince1970 {
if eventDay >= todayLong {
height = 280
}
}
return height
}
I see a problem. You are using regular cell as section footer and there is why you see unpredictable behaviour. You should use UITableViewHeaderFooterView instead. It is if you really need such design. Better solution will be to remove footer and make it cell instead.
I am making UITableView custom cell because height also changed per cell.
This is my code for initialize cell and after that i want to add UITextView as Subview.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
let dictAd = self.arrAllQuestions[indexPath.row] as! NSDictionary
let fontNew = UIFont(name: "AvenirNext-Regular", size: 19.0)
let strA = "\(indexPath.row+1). \(dictAd.object(forKey: "Title")!)"
let heightTitle = self.heightForView(text: strA, font: fontNew!, width: tableView.frame.size.width-16)
return heightTitle+5;
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let dictAd = self.arrAllQuestions[indexPath.row] as! NSDictionary
let cell = tableView.dequeueReusableCell(withIdentifier: "quesionCell", for: indexPath) as! QuestionCell
let fontNew = UIFont(name: "AvenirNext-Regular", size: 19.0)
let strA = "\(indexPath.row+1). \(dictAd.object(forKey: "Title")!)"
cell.lblName.font = fontNew
cell.lblName.text = strA
cell.lblName.numberOfLines = 0
let heightTitle = self.heightForView(text: strA, font: fontNew!, width: tableView.frame.size.width-16)
var frame = cell.lblName.frame as CGRect
frame.size.height = heightTitle; //you need to adjust this value
cell.lblName.frame = frame;
let txtView = AnimatableTextView(frame: CGRect(x: 8, y: cell.lblName.frame.origin.y+cell.lblName.frame.size.height+5, width: tableView.frame.size.width-16, height: 25))
txtView.borderWidth = 1.0
txtView.borderColor = UIColor.lightGray
txtView.cornerRadius = 4.0
cell.contentView.addSubview(txtView)
return cell
}
You can see output below.
It seems that the height calculation in your heightForRowAtIndexPath is not proper. Consider using self-sizing cells using UITableViewAutomaticDimension and Autolayout to solve your issues. Here is a great tutorial that can help you to get started. One more suggestion if you are using UITextView or subclass of the same in a cell and you want it to take the height of the content set its scrollable property to false.
Declare this in the viewDidLoad
Hope this will help you
tableView.estimatedRowHeight = 44
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
}
}
}
I am new to iOS development. I have a problem with my cell sizing. I am not using Auto-Laylout feature. My current TableView cell looks something like this. I want to make the label size which is selected in the image to be extended dynamically based on the text content of that Label.
Thanks in advance.
i think the swift version like below, it calculates nearer values not the exact height, for example
class ViewController: UIViewController,UITableViewDataSource,UITableViewDelegate {
var messageArray:[String] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
messageArray = ["One of the most interesting features of Newsstand is that once an asset downloading has started it will continue even if the application is suspended (that is: not running but still in memory) or it is terminated. Of course during while your app is suspended it will not receive any status update but it will be woken up in the background",
"In case that app has been terminated while downloading was in progress, the situation is different. Infact in the event of a finished downloading the app can not be simply woken up and the connection delegate finish download method called, as when an app is terminated its App delegate object doesn’t exist anymore. In such case the system will relaunch the app in the background.",
" If defined, this key will contain the array of all asset identifiers that caused the launch. From my tests it doesn’t seem this check is really required if you reconnect the pending downloading as explained in the next paragraph.",
"Whale&W",
"Rcokey",
"Punch",
"See & Dive"]
}
in the above we have an array which contains string of different length
func numberOfSectionsInTableView(tableView: UITableView) -> Int
{
return 1;
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return messageArray.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
var cell:UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("CELL") as? UITableViewCell;
if(cell == nil)
{
cell = UITableViewCell(style:UITableViewCellStyle.default, reuseIdentifier: "CELL")
cell?.selectionStyle = UITableViewCellSelectionStyle.none
}
cell?.textLabel.font = UIFont.systemFontOfSize(15.0)
cell?.textLabel.sizeToFit()
cell?.textLabel.text = messageArray[indexPath.row]
cell?.textLabel.numberOfLines = 0
return cell!;
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
var height:CGFloat = self.calculateHeightForString(messageArray[indexPath.row])
return height + 70.0
}
func calculateHeight(inString:String) -> CGFloat
{
let messageString = inString
let attributes : [String : Any] = [NSFontAttributeName : UIFont.systemFont(ofSize: 15.0)]
let attributedString : NSAttributedString = NSAttributedString(string: messageString, attributes: attributes)
let rect : CGRect = attributedString.boundingRect(with: CGSize(width: 222.0, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil)
let requredSize:CGRect = rect
return requredSize.height
}
try it in a new project just add the tableview and set its datasource and delegate and past the code above and run
the result will be like below
Try to override methods:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
Complete solution:
In iOS 8, Apple introduces a new feature for UITableView known as Self Sizing Cells. Prior to iOS 8, if you displayed dynamic content in table view with varied row, you need to calculate the row height on your own.
In summary, here are the steps to implement when using self sizing cells:
• Add auto layout constraints in your prototype cell
• Specify the estimatedRowHeight of your table view
• Set the rowHeight of your table view to UITableViewAutomaticDimension
Expressing last two points in code, its look like:
tableView.estimatedRowHeight = 43.0;
tableView.rowHeight = UITableViewAutomaticDimension
You should add it in viewDidLoad method.
With just two lines of code, you instruct the table view to calculate the cell’s size matching its content and render it dynamically. This self sizing cell feature should save you tons of code and time.
In Attributes Inspector of your UILabel, change Lines value to 0, so label will automatically adjust the number of lines to fit the content.
Please note that first point is required and remember that you cannot use self sizing cell without applying auto layout.
If you are note familiar with auto layout, please read this, it will be enough:
https://developer.apple.com/library/mac/recipes/xcode_help-IB_auto_layout/chapters/pin-constraints.html
Or, easier way to set auto layout, but maybe not be what you exactly expected is to clear all of your constraints, go to Resolve Auto Layout Issues and for All Views click on Reset to Suggested Constraints.
Just add this in Viewdidload
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
func calculateHeight(inString:String) -> CGFloat
{
let messageString = inString
let attributes : [NSAttributedString.Key : Any] = [NSAttributedString.Key(rawValue:
NSAttributedString.Key.font.rawValue) : UIFont.systemFont(ofSize:
15.0)]
let attributedString : NSAttributedString = NSAttributedString(string: messageString, attributes: attributes)
let rect : CGRect = attributedString.boundingRect(with: CGSize(width: 222.0, height: CGFloat.greatestFiniteMagnitude),
options: .usesLineFragmentOrigin, context: nil)
let requredSize:CGRect = rect
return requredSize.height
}