Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITableView with variable cell height: Working in IB but not programmatically

Tags:

TL;DR

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.

Goal

I'm trying to make a suggestion bar for a custom Mongolian keyboard. Mongolian is written vertically. In Android it looks like this:

enter image description here

Progress

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:

  • How to make a custom table view cell
  • Getting variable height to work with in a table view with a standard UILabel
  • Getting intrinsic content size to work for a custom view
  • Using a programmatically created UITableViewCell
  • Set constraints programmatically

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.

Works in IB

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.

enter image description here

In IB

I set constraints to pin the top and bottom as well as centering the label.

enter image description here

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! } 

Doesn't work programmatically

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.

enter image description here

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:

  • iOS8 - constraints ambiguously suggest a height of zero
  • Detected a case where constraints ambiguously suggest a height of zero
  • custom UITableviewcell height not set correctly
  • ios 8 (UITableViewCell) : Constraints ambiguously suggest a height of zero for a tableview cell's content view

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     } } 

Supplemental Code

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)     } } 

Update

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:

  • ViewController.swift
  • MyCustomCell.swift
  • UIMongolSingleLineLabel.swift
like image 551
Suragch Avatar asked Apr 11 '16 10:04

Suragch


People also ask

How do you add space between Tableview cells?

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.


1 Answers

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.

like image 174
Sulthan Avatar answered Oct 21 '22 13:10

Sulthan