I have dynamically changing tableview cells that have two labels on top of each other. The top label always has some text, but the bottom label sometimes does not. Upon loading the tableview, the cells without text for the bottom label disappear entirely with only their header showing. If I scroll down and come back up, these previously missing cells appear correctly.

I think that this has something to do with my constraints, but when I try to adjust the constraints, I've only made things worse.

How do I make the constraints such that the cells will always appear at first upon loading the screen, including when the bottom label has no text?

Here's the code for the tableview cell:

class TransactionHistoryTableViewCell: UITableViewCell {

private var itemView = UIView()
private var itemNameLabel = UILabel()
private var sizeLabel = UILabel()

func setup(_ lineItem: MenuItem) {
    contentView.backgroundColor = .white
    configureItemNameLabel(lineItem)
    configureSizeLabel(lineItem)
    configureTransactionView(lineItem)
}

private func configureTransactionView(_ lineItem: MenuItem) {
    itemView.clipsToBounds = true
    itemView.layer.cornerRadius = 5
    itemView.layer.borderColor = UIColor.lightGray.cgColor
    itemView.layer.borderWidth = 0.5
    itemView.backgroundColor = .white
    contentView.addSubview(itemView)
    itemView.translatesAutoresizingMaskIntoConstraints = false
    itemView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
    itemView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.8).isActive = true
    itemView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4).isActive = true
    itemView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4).isActive = true
}

private func returnText(_ menuItem: MenuItem) -> String {
    var text = ""
    guard let modifiers = menuItem.modifiers else { print("modifiers are nil"); return "" }
    if modifiers.isEmpty && menuItem.quantity == 1 {
        text = ""
    } else if !modifiers.isEmpty && menuItem.quantity == 1 {
        text = generateModifierText(menuItem)
    } else if !modifiers.isEmpty && menuItem.quantity > 1 {
        let theText = generateModifierText(menuItem)
        text = "\(theText); Quantity: \(menuItem.quantity)"
    } else {
        text = "Quantity: \(menuItem.quantity)"
    }
    return text
}

private func configureItemNameLabel(_ lineItem: MenuItem) {
    itemNameLabel.text = lineItem.name
    let fontSize = getSize(large: 14, medium: 13.5, small: 12)
    itemNameLabel.font = UIFont(name: AppFont.secondary.name, size: fontSize)
    itemNameLabel.numberOfLines = 0
    itemView.addSubview(itemNameLabel)
    itemNameLabel.translatesAutoresizingMaskIntoConstraints = false
    itemNameLabel.leftAnchor.constraint(equalTo: itemView.leftAnchor, constant: 15).isActive = true
    let verticalOffset = getSize(large: 10, medium: 7, small: 5)
    itemNameLabel.topAnchor.constraint(equalTo: itemView.topAnchor, constant: verticalOffset).isActive = true
    itemNameLabel.widthAnchor.constraint(equalToConstant: 195).isActive = true
    itemView.addSubview(sizeLabel)
    itemNameLabel.bottomAnchor.constraint(equalTo: sizeLabel.topAnchor, constant: -3).isActive = true
}

func heightForView(text:String, font:UIFont, width:CGFloat) -> CGFloat{
    let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
    label.numberOfLines = 0
    label.lineBreakMode = NSLineBreakMode.byWordWrapping
    label.font = font
    label.text = text
    label.sizeToFit()
    return label.frame.height
}

override func prepareForReuse() {
    super.prepareForReuse()
    itemNameLabel.text = ""
    sizeLabel.text = ""
}

private func configureSizeLabel(_ menuItem: MenuItem) {
    guard let modifiers = menuItem.modifiers else { print("modifiers are nil"); return }
    if modifiers.isEmpty && menuItem.quantity == 1 {
        sizeLabel.text = ""
    } else if !modifiers.isEmpty && menuItem.quantity == 1 {
        sizeLabel.text = generateModifierText(menuItem)
    } else if !modifiers.isEmpty && menuItem.quantity > 1 {
        let text = generateModifierText(menuItem)
        if text != "" {
            sizeLabel.text = "\(text); Quantity: \(menuItem.quantity)"
        } else {
            sizeLabel.text = "Quantity: \(menuItem.quantity)"
        }
    } else {
        sizeLabel.text = "Quantity: \(menuItem.quantity)"
    }
    sizeLabel.backgroundColor = .white
    sizeLabel.textColor = .gray
    sizeLabel.numberOfLines = 0
    let fontSize = getSize(large: 11, medium: 10.5, small: 9.5)
    sizeLabel.font = UIFont(name: AppFont.secondary.name, size: fontSize)
    sizeLabel.translatesAutoresizingMaskIntoConstraints = false
    sizeLabel.topAnchor.constraint(equalTo: itemNameLabel.bottomAnchor, constant: 3).isActive = true
    sizeLabel.leftAnchor.constraint(equalTo: itemNameLabel.leftAnchor).isActive = true
    sizeLabel.widthAnchor.constraint(equalToConstant: 200).isActive = true
    sizeLabel.bottomAnchor.constraint(equalTo: itemView.bottomAnchor, constant: -3).isActive = true
}

private func generateModifierText(_ menuItem: MenuItem) -> String {
    var text = ""
    guard let modifiers = menuItem.modifiers else { return "" }
    var optionNames = [String]()
    for modifier in modifiers {
        if !modifier.options.isEmpty {
            for options in modifier.options{
                if options.name.uppercased() != "NONE" {
                    optionNames.append(options.name)
                }
            }
        }
    }
    for x in 0..<optionNames.count {
        if x != optionNames.count - 1 {
            text += "\(optionNames[x]), "
        } else {
            text += "\(optionNames[x])"
        }
    }
    return text
}

private func generateSizeLabelFontSize() -> CGFloat {
    return getSize(large: 11, medium: 10, small: 9.5)
}

}

Code from the view controller:

override func viewDidLoad() {
    super.viewDidLoad()
    configureNavBar()
    configureTableView()
    getOrders()
    NotificationCenter.default.addObserver(self, selector: #selector(popToRootVC), name: NSNotification.Name(rawValue: "PopToRootVCFromSettingsVC"), object: nil)
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "transactionHistoryCell", for: indexPath) as? TransactionHistoryTableViewCell else { getOrders(); return UITableViewCell() }
    if let lineItem = orders[indexPath.section].lineItems?[indexPath.row] {
        cell.setup(lineItem)
    }
    return cell
}

private func getOrders() {
    let service = TransactionService()
    service.getTransactionData(completion: { (orders) in
        guard let orders = orders else { self.handleNoOrderHistory(); return }
        let filteredOrders = orders.filter{ $0.status == "NEW" || $0.status == "IN_PROGRESS" || $0.status == "READY" || $0.status == "COMPLETE" }
        if filteredOrders.isEmpty {
            DispatchQueue.main.async {
                self.handleNoOrderHistory()
                return
            }
        } else {
            DispatchQueue.main.async {
                self.orders = filteredOrders.sorted{ $0.date > $1.date }
                self.noOrdersView.isHidden = true
                self.tableView.reloadData()
            }
        }
    })
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return UITableView.automaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return 85
}

The compiler gives warnings like this one:

Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
    (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    NSAutoresizingMaskLayoutConstraint:0x600001e972a0 h=--& v=--& UILabel:0x7f9a0050b780.midY == 0   (active),
    NSAutoresizingMaskLayoutConstraint:0x600001e97610 h=--& v=--& UILabel:0x7f9a0050b780.height == 0   (active),
    NSLayoutConstraint:0x600001e8bca0 V:|-(5)-[UILabel:0x7f9a0050b490'5\" Focaccia Everything']   (active, names: '|':UIView:0x7f9a0050b2b0 ),
    NSLayoutConstraint:0x600001d63660 UILabel:0x7f9a0050b490'5\" Focaccia Everything'.bottom == UILabel:0x7f9a0050b780.top - 3   (active)
)

Will attempt to recover by breaking constraint 
NSLayoutConstraint:0x600001d63660 UILabel:0x7f9a0050b490'5" Focaccia Everything'.bottom == UILabel:0x7f9a0050b780.top - 3   (active)

3 Answers

0
matt On

The problem is that before you add any subview and apply constraints, you need to set its translatesAutoresizing... to false.

So, you call configureItemNameLabel and add the sizeLabel, before you call configureSizeLabel and fix its translatesAutoresizing....

0
Community On

When you use dynamic height to build a tableview, and got constraint warning in console like this, it's always because the app can't get the current height from your given constraint.

The most important thing for use dynamic height with cell, is you need give correct vertically constraint from top to bottom, for collection cell, you need give correct horizontal and vertically constraint.

For example, i need show three label in one cell. like this

--------------------------
    LABLE_1
    LABEL_2
    LABEL_3   
--------------------------

We need add

  • LABEL_1 with view's top
  • LABEL_2's top with LABEL_1's bottom
  • LABEL_3's top with LABEL_2's bottom
  • LABEL_3's bottom with view's bottom

And another point is some UI component can adjust height by itself, like UIButton, UILabel, etc. so if you just want to show content, you don't need to give height constraint for some kind of component. and if you need do this, you can do edit constraint's priority to 999, it's always can fix warning.

The reason i understand is if you want to use dynamic height, component need decide height by themself, so we give tableview estimatedRowHeight, and some constraint about relationship between components. if you really need give height constraint, please use constraint's priority.

The last point is if component can't adjust height by itself, like UIView, you can give a height constraint and change height's priority or give top, bottom constraint with other and some components it include can adjust height by themself and some component have correct vertically constraint with the view

0
michaeldebo On

I had to change the heightForRowAt from automaticDimensions to:

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    //return UITableView.automaticDimension
    guard let lineItem = self.orders[indexPath.section].lineItems?[indexPath.row] else { return 0 }
    let fontSize = getSize(large: 14, medium: 13.5, small: 12)
    guard let topFont = UIFont(name: AppFont.secondary.name, size: fontSize) else { return 0 }
    let heightForTopLabel = heightForView(text: lineItem.name, font: topFont, width: 195)
    let text = returnText(lineItem)
    let bottomFontSize = getSize(large: 11, medium: 10.5, small: 9.5)
    guard let font = UIFont(name: AppFont.secondary.name, size: bottomFontSize) else { return 0 }
    let heightForBottomLabel = heightForView(text: text, font: font, width: 200)
    let padding = getSize(large: 27.5, medium: 14, small: 21)
    let height = heightForTopLabel + heightForBottomLabel + padding
    return height
}

func heightForView(text:String, font:UIFont, width:CGFloat) -> CGFloat{
    let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
    label.numberOfLines = 0
    label.lineBreakMode = NSLineBreakMode.byWordWrapping
    label.font = font
    label.text = text
    label.sizeToFit()
    return label.frame.height
}

private func returnText(_ menuItem: MenuItem) -> String {
    var text = ""
    guard let modifiers = menuItem.modifiers else { print("modifiers are nil"); return "" }
    if modifiers.isEmpty && menuItem.quantity == 1 {
        text = ""
    } else if !modifiers.isEmpty && menuItem.quantity == 1 {
        text = generateModifierText(menuItem)
    } else if !modifiers.isEmpty && menuItem.quantity > 1 {
        let theText = generateModifierText(menuItem)
        text = "\(theText); Quantity: \(menuItem.quantity)"
    } else {
        text = "Quantity: \(menuItem.quantity)"
    }
    return text
}

private func generateModifierText(_ menuItem: MenuItem) -> String {
    var text = ""
    guard let modifiers = menuItem.modifiers else { return "" }
    var optionNames = [String]()
    for modifier in modifiers {
        if !modifier.options.isEmpty {
            for options in modifier.options{
                if options.name.uppercased() != "NONE" {
                    optionNames.append(options.name)
                }
            }
        }
    }
    for x in 0..<optionNames.count {
        if x != optionNames.count - 1 {
            text += "\(optionNames[x]), "
        } else {
            text += "\(optionNames[x])"
        }
    }
    return text
}