I've scoured through all of the questions here regarding a similar issue and I still seem to have this problem no matter what I do. I'm on iOS 8.3.
You can find the full code at: https://github.com/DanielRakh/CellHeightTest.git
I have a cell that dynamically resizes based on the content of a UILabel that varies from single to multi-line text. Everything seems to be fine initially but when I start scrolling fast and quickly switch directions
I get a warning in the debugger:
2015-03-10 02:02:00.630 CellHeight[21115:3711275] Warning once only:
Detected a case where constraints ambiguously suggest a height of zero
for a tableview cell's content view. We're considering the collapse
unintentional and using standard height instead.
Here's how it looks in the simulator. I've marked the discrepancies in red:
Here's the simple setup in IB where I have a UILabel pinned top(11), bottom(11), leading(8), and trailing(8) with a custom cell at 45pts:
Here is my code. The cell is a custom subclass thats pretty much empty except for the IBOutlet for the label:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var arr = [String]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
for _ in 1...10 {
arr.append("This is an amount of text.")
arr.append("This is an even larget amount of text. This should move to other lines of the label and mace the cell grow.")
arr.append("This is an even larger amount of text. That should move to other lines of the label and make the cell grow. Crossing fingers. This is an even larger amount of text. That should move to other lines of the label and make the cell grow. Crossing fingers.")
}
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 45.0
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
tableView.reloadData()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arr.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! TstTableViewCell
cell.tstLabel.text = arr[indexPath.row]
return cell
}
}
EDIT: I've found that if I created the cells programmatically and set the constraints programmatically everything seems to work as intended. Weird. This seems to be an IB + AL issue.
Related
I am making a simple contacts application with controllers: ContactTableViewController and custome cell: ContactTableViewCell. I created a custom table view cell where the style is custom, identifier is ContactTableViewCell, and class is also ContactTableViewCell.
The cell has two UILabel fields, which are exposed to ContactTableViewCell.swift class as follows:
import UIKit
class ContactTableViewCell: UITableViewCell {
#IBOutlet weak var name: UILabel!
#IBOutlet weak var info: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
print("cell loaded with name: ", name)
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Now I want to display them in my ContactViewController, and the relevant part of my controller looks like this:
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
// here we communicate with parts of the app that owns the data
override func tableView(_ tableView : UITableView
, cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let id = "ContactTableViewCell"
// deque a cell as an instance of ContactTableViewCell
guard let cell = tableView.dequeueReusableCell(withIdentifier: id, for : indexPath)
as? ContactTableViewCell else {
fatalError("dequed cell is not instance of ContactTableViewCell")
}
cell.name.text = "hello"
cell.info.text = "hello information"
However, when I run the simulation I only see two rows of "hello", even though it should be something like:
hello
hello information
repeated twice. What is the problem here?
Most probably you don't have enough room to fit both labels. You can either set the row height in storyboard by:
Selecting your tableview
Go to size inspector
Change the Row Height
OR
You can implement tableView(_:heightForRowAt:) method in your tableView's delegate. i.e.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// return desired height for your cell
return 90.0
}
It is a contraint issue. You have to define height, width, x and y positioning for both the name UILabel and the info UILabel. Font size places a dominant role for height with UILabels and width can be based on the number of characters in the UILabel x font size so you do not have to explicity express height and width constraints for a UILabel, but for a UIButton you would have to explicitly define constraints for x,y,width,height. For UILabel we only have to define x and y constraints.
What happens when you do not clearly define the constraints for your UI elements is that the rendering of your View will have unpredictable results and UI elements quite often just do not appear on the view.
It looks like, from your code, you are using XCode designer to add your UILabels so that is what you can use to add constraints. You can also add constraints programmatically as well. But I am pretty sure you are using XCode Storyboard designer.
Whether programmatically or through XCode designer you need add a constraint for the name UILabel to the top and left of the super view, you can reposition later, and then constrain x and y alignment of the info UILabel to the name UILabel, horizontally aligning to the name UILabel and +8 vertical spacing to the bottom of the name UILabel, this will place the info UILabel below and centered to the name UILabel.
See this guide: https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithConstraintsinInterfaceBuidler.html
And this Stackoverflow answer: https://stackoverflow.com/a/34448801/1258525
Not the first two constraints I have circled in this picture, this defines the x and y for the Video Quality UILabel:
And then see how the next label below, Onset Threshold constraints itself to the leading edge of the Video Quality label and then the divider below the VideoQuality label:
My theory the 2nd label is truncated not getting sufficient width, and may be the number of lines in the second label is 1. Hence try setting number of line to 0 and check the width and auto layout constraints.
I have a UITableViewController where the cell's self sized correctly using Xcode 8 and Swift 3. Now that I'm using Xcode 9 and Swift 4, they aren't expanding and are just using the default height of 44.
(I have about a sentence or two in each UITableViewCell)
I was using this before:
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
... but was able to comment it out because per Updating Your App for iOS 11 said that the default would be self-sizing now:
I've tried playing around with changing the deployment target to iOS 11, playing around in Storyboard (but I'm using a Table View Cell style Basic so there is not much AutoLayout to be done), and I can't figure out what is going on.
I have the UILabel title set to 0 Lines, and have Line Break Word Wrap, but still not getting anywhere close to getting the cell to expand based on the text content inside of it in Xcode 9. Any ideas?
Thanks!
Edit:
Here's the options (that I don't have) for pinning since it is a Basic cell:
I had the same problem and solved it with to lines of code:
class MyTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = UITableViewAutomaticDimension
tableView.rowHeight = UITableViewAutomaticDimension
}
Maybe it is a bug in Xcode.
Update
New in Xcode 9 beta 3:
Interface Builder now supports setting the estimatedRowHeight of UITableView. This allows self-sizing table cells by setting the estimated height to a value other than zero, and is on by default. (17995201)
I had the same broken table view issue. Fix was just one click.
Go to your xib or storyboard scenes with table views, go to the size inspector, and you'll see the table view heights (even on dynamic table views) as 44, and sections will be 22. Just click "automatic" and boom, it will present as expected.
Note that I also specify the following in viewDidLoad of the UITableViewController subclass (layoutSubviews solves issues with the first load of a tableViewController not positioning correctly in relation to a non-translucent navBar).
self.tableView.estimatedRowHeight = 180;
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[self.tableView layoutSubviews];
In addition to
tableView.estimatedRowHeight = UITableViewAutomaticDimension
tableView.rowHeight = UITableViewAutomaticDimension
you should set a height constraint for the contentView of the tabeleViewCell.
class CustomTableViewCell: UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let height: CGFloat = 200
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
I got the same issue and I read about it in many documentation, satisfying answer was something like this, You have to check both options in order to get proper height, because estimated height is needed for initial UI setup like scrollview bars and other such stuff.
Providing a nonnegative estimate of the height of rows can improve the performance of loading the table view. If the table contains variable height rows, it might be expensive to calculate all their heights when the table loads. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
When you create a self-sizing table view cell, you need to set this property and use constraints to define the cell’s size.
The default value is 0, which means there is no estimate. (Apple Documentation)>
see this image for storyboard
Also note that there is a bug in xCode 9, when you try to apply Lazy loading in automatic height calculation, it will scroll unexpectedly, so I'll recommend you to use programmatic way in this regard.
self.postTableView.estimatedRowHeight = 200;
self.postTableView.rowHeight = UITableViewAutomaticDimension;
something Like this. Thanks!
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var tblview: UITableView!
var selectindex = -1
var arrnumber = ["1","2","3","4","5"]
var image = ["index.jpg","rose-blue-flower-rose-blooms-67636.jpeg","index.jpg","rose-blue-flower-rose-blooms-67636.jpeg","index.jpg"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrnumber.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tblview.dequeueReusableCell(withIdentifier: "cell", for: indexPath)as! ExpandableTableViewCell
cell.lblnumber.text = arrnumber[indexPath.row]
cell.img.image = UIImage(named: image[indexPath.row] as! String)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if (selectindex == indexPath.row)
{
return 250
}
else{
return 60
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if(selectindex == indexPath.row)
{
selectindex = -1
}else{
selectindex = indexPath.row
}
self.tblview.beginUpdates()
self.tblview.endUpdates()
}
}
For me, Safe Area was checked. Unchecking "Safe Area" did the work for me.
I have a very long list in my app. Each cell has multiple lines of text and a large picture, making scrolling through this list of 200+ items very tedious.
I wanted to implement a solution that would make scrolling easier, without implementing alphabetical scrolling as shown in the contacts app (since this would prevent me from allowing fast scrolling).
I wanted to implement a solution where, if you scrolling on the right side (to the point where your finger is hovering over the scroll bar on the right), than it would work the same as if you were clicking and dragging on the scroll bar on a desktop (meaning you could drag the scroll bar to the top or bottom very very quickly).
Is this a viable design for iOS? If so, what would be the best way to go about implementing it?
You could use a UISlider for that purpose. Here I created an example of a small tableview with 1000 rows and a slider to scroll across them: example
You can customize the UISlider to make it smaller, vertical, or whatever you want. Here's the viewControllers code, in case the file goes offline:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var slider: UISlider!
#IBOutlet weak var tableView: UITableView!
private let numberOfRows = 1000
override func viewDidLoad() {
super.viewDidLoad()
slider.maximumValue = Float(numberOfRows - 1) //due to rows goes from 0 to 999
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberOfRows
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier") as! UITableViewCell
cell.textLabel?.text = "\(indexPath.row)"
return cell;
}
#IBAction func sliderValueChanged(sender: UISlider) {
let positionToScroll = sender.value
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: Int(positionToScroll), inSection: 0), atScrollPosition: UITableViewScrollPosition.None, animated: false)
}
}
I've making a simple UITableViewCell prototype in a storyboard, but I'm having some issues with the self-sizing aspect. As you can see in the image below, the cell size increases, but the text doesn't move down. (It has 0 lines.) Here's the table's view controller.
import UIKit
class ViewController: UITableViewController {
let events = ["Kaufman Family Finished Project Reception", "Home Remodelers' Survival Guide", "Mori Family Mid-Construction Showcase"]
let locations = ["La Cañada Flintridge", "Claremont", "Claremont"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 70.0
tableView.rowHeight = UITableViewAutomaticDimension
tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return events.count;
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("EventCell") as EventTableViewCell
cell.eventName.text = events[indexPath.row]
cell.eventLocation.text = locations[indexPath.row]
return cell
}
}
Obviously I can't provide you with dozens of screenshots of the storyboard, so comment if you need other information. Here's some basic pictures though.
Hard to tell from the constraint diagram... for this arrangement to work you need:
Event Name label set to 0 lines (you said you do)
Event Name label set to word wrap (by default it isn't)
Event Name label constrained to top of cell and leading/trailing edges of cell
Event Name label bottom constrained to top of Event Location label
Same conditions for Event Location label - word wrap, 0 lines, if you want it to have variable number of lines
Event Location label constrained to bottom of cell and leading/trailing edges of cell
This should allow both labels to dynamically size their height and for the cell to size itself to accommodate the label heights and margins.
One last thing... you are calling sizeToFit on the label after setting the text, right?
Here are some images from a project of mine where resizing labels work:
Final result:
"Questions Here" is the label being changed.
Size Inspector
Properties Inspector
Currently I am messing around with swift and dynamic table cell heights. I developed a simple app for iOS8.1 on xcode6.1: https://github.com/ArtworkAD/DynamicCellTest
So to achieve a cell height that stretches with the cell's content I do the following:
in storyboard set label lines to 0
set labels font to system
set constraints for label in cell
add self.tableView.rowHeight = UITableViewAutomaticDimension
don't override heightForRowAtIndex method
Minimal code is needed:
class MyTableViewController: UITableViewController {
var entries:Array<String> = [String]()
override func viewDidLoad() {
super.viewDidLoad()
//create dummy content
var i = 0
while i < 10 {
entries.append("\(i) Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor")
entries.append("\(i+1) Lorem ipsum dolor sit amet")
i = i + 2;
}
self.tableView.rowHeight = UITableViewAutomaticDimension
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.entries.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("basic_cell", forIndexPath: indexPath) as UITableViewCell
var label = cell.viewWithTag(13)
if let unwrappedLabel = label as? UILabel {
unwrappedLabel.text = self.entries[indexPath.row]
}
return cell
}
}
The left image shows the result of the above code. The cell height grows with the content of the label, all nice. However when you click on the disclosure indicator to the detail view and move back again, you get the right image. Why is this happening??
A bad solution for this problem is to override this methods:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
self.tableView.reloadData()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.tableView.reloadData()
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
self.tableView.reloadData()
}
This way the above problem is solved, but this solution seems not right? A side effect of it is, that when self.tableView.reloadData() is called the table view port jumps to the first cell which doesn't look nice.
Does anyone has an idea what I am doing wrong? Feel free to clone my repo https://github.com/ArtworkAD/DynamicCellTest and test it out.
Adding this seems that it is able to fix rotation problem.
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
self.tableView.estimatedRowHeight = 36.5
}
However, I have another case that there are 4 labels in the cell, which has the same rotation problem, adding this is not enough and I ended up replacing the self.tableView.estimatedRowHeight = 36.5 with reloading visible cells.
I've just solved exactly that problem by overriding tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) in UITableViewDelegate object.
func tableView(tableView: UITableView,
estimatedHeightForRowAtIndexPath indexPath: NSIndexPath)
-> CGFloat {
return 200
}
Changing the exact returned value apparently have no effect. The cells are always properly self-sizing. I've got no idea why providing estimated height causes the autolayout to kick in, but it does. iOS 8.1.2, Xcode 6.1.
The link provides a good explanation of what needs to be done.
Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
For your specific problem you are missing two lines of code.
In viewDidLoad under your tableView.rowHeight add self.tableView.estimatedRowHeight = 64
The above will provides allow the tableView to assign estimated height values for offscreen cells to enhance performance and it also calculates the scroll bar height.
The main problem is you are missing unwrappedLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds) in cellForRowAtIndexPath this tells the label its preferred maximum width so it can calculate the height it requires for multi-line text. Usually you would do this in the table view cell subclass.
I have tested this in your project and it works
You have to set estimatedRowHeight:
// setup automatic row height
self.tableView.rowHeight = UITableViewAutomaticDimension
// whatever you think is the approximate row height
self.tableView.estimatedRowHeight = 44
Also came across an alternative here http://useyourloaf.com/blog/2014/08/07/self-sizing-table-view-cells.html. A comment mentions -
Simply make the cell layout its subviews before returning it from cellForRowAtIndexPath:
[cell setNeedsDisplay];
[cell layoutIfNeeded];