My programmatically created table view cells are not resizing according to the intrinsic content height of their custom views, even though I am using UITableViewAutomaticDimension
and setting both the top and bottom constraints.
The problem probably lies in my implementation of the UITableViewCell
subclass. See the code below under Doesn't work programmatically > Code > MyCustomCell.swift.
I'm trying to make a suggestion bar for a custom Mongolian keyboard. Mongolian is written vertically. In Android it looks like this:
I've learned that I should use a UITableView
with variable cell heights, which is available starting with iOS 8. This requires using auto layout and telling the table view to use automatic dimensions for the cell heights.
Some things I've had to learn along the way are represented in my recent SO questions and answers:
UILabel
UITableViewCell
So I have come to the point where I have the vertical labels that support intrinsic content size. These labels go in my custom table view cells. And as described in the next section, they work when I do it in the storyboard, but not when I create everything programmatically.
In order to isolate the problem I created two basic projects: one for where I use the storyboard and one where I do everything programmatically. The storyboard project works. As can be seen in the following image, each table view cell resizes to match the height of custom vertical label.
In IB
I set constraints to pin the top and bottom as well as centering the label.
Code
ViewController.swift
import UIKit class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { let myStrings: [String] = ["a", "bbbbbbb", "cccc", "dddddddddd", "ee"] let cellReuseIdentifier = "cell" @IBOutlet var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self tableView.dataSource = self tableView.estimatedRowHeight = 44.0 tableView.rowHeight = UITableViewAutomaticDimension } // number of rows in table view func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.myStrings.count } // create a cell for each table view row func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell cell.myCellLabel.text = self.myStrings[indexPath.row] return cell } // method to run when table view cell is tapped func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { print("You tapped cell number \(indexPath.row).") } }
MyCustomCell.swift
import UIKit class MyCustomCell: UITableViewCell { @IBOutlet weak var myCellLabel: UIMongolSingleLineLabel! }
Since I want the suggestion bar to be a part of the final keyboard, I need to be able to create it programmatically. However, when I try to recreate the above example project programmatically, it isn't working. I get the following result.
The cell heights are not resizing and the custom vertical labels are overlapping each other.
I also get the following error:
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.
This error has been brought up before multiple times on Stack Overflow:
However, the problem for most of those people is that they were not setting both a top and bottom pin constraint. I am, or at least I think I am, as is shown in my code below.
Code
ViewController.swift
import UIKit class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { let myStrings: [String] = ["a", "bbbbbbb", "cccc", "dddddddddd", "ee"] let cellReuseIdentifier = "cell" var tableView = UITableView() override func viewDidLoad() { super.viewDidLoad() // Suggestion bar tableView.frame = CGRect(x: 0, y: 20, width: view.bounds.width, height: view.bounds.height) tableView.registerClass(MyCustomCell.self, forCellReuseIdentifier: cellReuseIdentifier) tableView.delegate = self tableView.dataSource = self tableView.estimatedRowHeight = 44.0 tableView.rowHeight = UITableViewAutomaticDimension view.addSubview(tableView) } // number of rows in table view func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.myStrings.count } // create a cell for each table view row func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell cell.myCellLabel.text = self.myStrings[indexPath.row] return cell } // method to run when table view cell is tapped func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { print("You tapped cell number \(indexPath.row).") } }
MyCustomCell.swift
I think the problem is probably in here since this is the main difference from the IB project.
import UIKit class MyCustomCell: UITableViewCell { var myCellLabel = UIMongolSingleLineLabel() override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) self.setup() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setup() { self.myCellLabel.translatesAutoresizingMaskIntoConstraints = false self.myCellLabel.centerText = false self.myCellLabel.backgroundColor = UIColor.yellowColor() self.addSubview(myCellLabel) // Constraints // pin top NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true // pin bottom NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true // center horizontal NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true } override internal class func requiresConstraintBasedLayout() -> Bool { return true } }
I'll also include the code for the custom vertical label that I used in both projects above, but since the IB project works, I don't think the main problem is here.
import UIKit @IBDesignable class UIMongolSingleLineLabel: UIView { private let textLayer = LabelTextLayer() var useMirroredFont = false // MARK: Primary input value @IBInspectable var text: String = "A" { didSet { textLayer.displayString = text updateTextLayerFrame() } } @IBInspectable var fontSize: CGFloat = 17 { didSet { updateTextLayerFrame() } } @IBInspectable var centerText: Bool = true { didSet { updateTextLayerFrame() } } // MARK: - Initialization override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } func setup() { // Text layer textLayer.backgroundColor = UIColor.yellowColor().CGColor textLayer.useMirroredFont = useMirroredFont textLayer.contentsScale = UIScreen.mainScreen().scale layer.addSublayer(textLayer) } override func intrinsicContentSize() -> CGSize { return textLayer.frame.size } func updateTextLayerFrame() { let myAttribute = [ NSFontAttributeName: UIFont.systemFontOfSize(fontSize) ] let attrString = NSMutableAttributedString(string: textLayer.displayString, attributes: myAttribute ) let size = dimensionsForAttributedString(attrString) // This is the frame for the soon-to-be rotated layer var x: CGFloat = 0 var y: CGFloat = 0 if layer.bounds.width > size.height { x = (layer.bounds.width - size.height) / 2 } if centerText { y = (layer.bounds.height - size.width) / 2 } textLayer.frame = CGRect(x: x, y: y, width: size.height, height: size.width) textLayer.string = attrString invalidateIntrinsicContentSize() } func dimensionsForAttributedString(attrString: NSAttributedString) -> CGSize { var ascent: CGFloat = 0 var descent: CGFloat = 0 var width: CGFloat = 0 let line: CTLineRef = CTLineCreateWithAttributedString(attrString) width = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, nil)) // make width an even integer for better graphics rendering width = ceil(width) if Int(width)%2 == 1 { width += 1.0 } return CGSize(width: width, height: ceil(ascent+descent)) } } // MARK: - Key Text Layer Class class LabelTextLayer: CATextLayer { // set this to false if not using a mirrored font var useMirroredFont = true var displayString = "" override func drawInContext(ctx: CGContext) { // A frame is passed in, in which the frame size is already rotated at the center but the content is not. CGContextSaveGState(ctx) if useMirroredFont { CGContextRotateCTM(ctx, CGFloat(M_PI_2)) CGContextScaleCTM(ctx, 1.0, -1.0) } else { CGContextRotateCTM(ctx, CGFloat(M_PI_2)) CGContextTranslateCTM(ctx, 0, -self.bounds.width) } super.drawInContext(ctx) CGContextRestoreGState(ctx) } }
The entire code for the project is all here, so if anyone is interested enough to try it out, just make a new project and cut and paste the code above into the following three files:
The way I achieve adding spacing between cells is to make numberOfSections = "Your array count" and make each section contains only one row. And then define headerView and its height. This works great.
The error is pretty trivial:
Instead of
self.addSubview(myCellLabel)
use
self.contentView.addSubview(myCellLabel)
Also, I would replace
// pin top NSLayoutConstraint(...).active = true // pin bottom NSLayoutConstraint(...).active = true // center horizontal NSLayoutConstraint(...).active = true
with
let topConstraint = NSLayoutConstraint(...) let bottomConstraint = NSLayoutConstraint(...) let centerConstraint = NSLayoutConstraint(...) self.contentView.addConstraints([topConstraint, bottomConstraint, centerConstraint])
which is more explicit (you have to specify the constraint owner) and thus safer.
The problem is that when calling active = true
on a constraint, the layout system has to decide to which view it should add the constraints. In your case, because the first common ancestor of contentView
and myCellLabel
is your UITableViewCell
, they were added to your UITableViewCell
, so they were not actually constraining the contentView
(constraints were between siblings not between superview-subview).
Your code actually triggered a console warning:
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.
Which made me to look immediately at the way the constraints are created for your label.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With