Search code examples
iosswiftuitableviewnslayoutconstraint

UITableViewCell doesn't use autolayout height when using insert cell into table


Background

I use purelayout to programmatically create my UITableViewCells, following the instructions here, which basically states that you gotta set the top/bottom constraints on a cell, then use

self.tableView.rowHeight = UITableViewAutomaticDimension;

to get it right:

enter image description here

Problem

Everything works fine, except when I insert a new row into a tableView. I get this effect: https://youtu.be/eTGWsxwDAdk

To explain: as soon as I click on one of the tip cells, the table is supposed to insert a driver tip row. However you'll notice that wen i refresh the section (by clicking on a tip box), all the cells height inexplicably increases, but when i click on tip boxes again, they go back to their normal height this is done with this code

self.tableView.beginUpdates()
self.tableView.reloadSections(IndexSet(integer:1), with: .automatic)
self.tableView.endUpdates()

this is the implementation of the cellfor row

// init table
self.tableView.register(OrderChargeTableViewCell.self,
                            forCellReuseIdentifier: OrderChargeTableViewCell.regularCellIdentifier)
self.tableView.register(OrderChargeTableViewCell.self,
                            forCellReuseIdentifier: OrderChargeTableViewCell.boldCellIdentifier)

self.tableView.estimatedRowHeight = 25
self.tableView.rowHeight = UITableViewAutomaticDimension

public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    var cell: OrderChargeTableViewCell?
    if (indexPath.row == filteredModel.count-1) {
        cell = tableView.dequeueReusableCell(withIdentifier: OrderChargeTableViewCell.boldCellIdentifier,
                                             for: indexPath) as? OrderChargeTableViewCell
    } else if (indexPath.row < filteredModel.count) {
        cell = tableView.dequeueReusableCell(withIdentifier: OrderChargeTableViewCell.regularCellIdentifier,
                                             for: indexPath) as? OrderChargeTableViewCell
    }

    // add data to cell labels
    return cell!
}

and this is the code for the UITableViewCell itself:

final class OrderChargeTableViewCell: UITableViewCell {

// MARK: - init
static let boldCellIdentifier = "TTOrderDetailBoldTableViewCell"
static let regularCellIdentifier = "TTOrderDetailRegularTableViewCell"

private var didSetupConstraints = false
.. 

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {

    self.keyLabel = TTRLabel()
    self.valueLabel = TTRLabel()

    if (reuseIdentifier == OrderChargeTableViewCell.regularCellIdentifier) {
        self.isCellStyleBold = false
    } else if (reuseIdentifier == OrderChargeTableViewCell.boldCellIdentifier) {
        self.isCellStyleBold = true
    } else {
        self.isCellStyleBold = false
        assertionFailure( "Attempt to create an OrderCharge cell with the wrong cell identifier: \(String(describing: reuseIdentifier))")
    }

    super.init(style: style, reuseIdentifier: reuseIdentifier)


    contentView.addSubview(keyLabel)
    contentView.addSubview(valueLabel)

}

override func updateConstraints()
{
    if !didSetupConstraints {
        if (isCellStyleBold) {
            self.applyBoldFormatting()
        } else {
            self.applyRegularFormatting()
        }

        didSetupConstraints = true
    }

    super.updateConstraints()
}
public func applyBoldFormatting() {
    keyLabel.font = .ttrSubTitle
    valueLabel.font = .ttrBody

    keyLabel.autoPinEdge(.leading, to: .leading, of: contentView, withOffset: 15)
    keyLabel.autoAlignAxis(.vertical, toSameAxisOf: contentView)

    keyLabel.autoPinEdge(.top, to: .top, of: contentView, withOffset: 8)
    keyLabel.autoPinEdge(.bottom, to: .bottom, of: contentView, withOffset: -8)

    valueLabel.autoPinEdge(.trailing, to: .trailing, of: contentView, withOffset: -15)
    valueLabel.autoAlignAxis(.baseline, toSameAxisOf: keyLabel)
}

public func applyRegularFormatting() {
    keyLabel.font = .ttrCaptions
    valueLabel.font = TTRFont.Style.standard(.h3).value

    keyLabel.autoPinEdge(.leading, to: .leading, of: contentView, withOffset: 15)
    keyLabel.autoAlignAxis(.vertical, toSameAxisOf: contentView)

    keyLabel.autoPinEdge(.top, to: .top, of: contentView, withOffset: 6)
    keyLabel.autoPinEdge(.bottom, to: .bottom, of: contentView, withOffset: -4)

    valueLabel.autoPinEdge(.trailing, to: .trailing, of: contentView, withOffset: -15)
    valueLabel.autoAlignAxis(.baseline, toSameAxisOf: keyLabel)
}

the driver tip row that gets inserted has the standard 44 pixel height of a cell: enter image description here

whereas the other (properly formatted) cells have the 25 height: enter image description here


Solution

  • While the StackOverflow answer you followed has a lot of up-votes, it appears you took one bullet point which was not very well explained (and may be outdated), and I think that may be what's causing your problems.

    You'll find many comments / posts / articles stating you should add your constraints in updateConstraints(), however, Apple's docs also state:

    Override this method to optimize changes to your constraints.

    Note

    It is almost always cleaner and easier to update a constraint immediately after the affecting change has occurred. For example, if you want to change a constraint in response to a button tap, make that change directly in the button’s action method.

    You should only override this method when changing constraints in place is too slow, or when a view is producing a number of redundant changes.

    I think you'll get much better results in what you're attempting if you add your subviews and their constraints when your cell is init'd.

    Here is a simple example which has a similar layout to what you've shown. It creates a table with 2 sections - first section has a row with a "show/hide" button. When tapped, the second section will add/remove the "Driver Tip" row.

    //
    //  InsertRemoveViewController.swift
    //
    //  Created by Don Mag on 12/4/18.
    //
    
    import UIKit
    
    struct MyRowData {
        var title: String = ""
        var value: CGFloat = 0.0
    }
    
    class OrderChargeTableViewCell: UITableViewCell {
    
        static let boldCellIdentifier: String = "TTOrderDetailBoldTableViewCell"
        static let regularCellIdentifier: String = "TTOrderDetailRegularTableViewCell"
    
        var keyLabel: UILabel = {
            let v = UILabel()
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
    
        var valueLabel: UILabel = {
            let v = UILabel()
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
    
        override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            commonInit()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
    
        func commonInit() -> Void {
    
            contentView.addSubview(keyLabel)
            contentView.addSubview(valueLabel)
    
            let s = type(of: self).boldCellIdentifier
    
            if self.reuseIdentifier == s {
    
                NSLayoutConstraint.activate([
                    keyLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8.0),
                    keyLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8.0),
                    keyLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15.0),
    
                    valueLabel.centerYAnchor.constraint(equalTo: keyLabel.centerYAnchor, constant: 0.0),
                    valueLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15.0),
                    ])
    
                keyLabel.font = UIFont.systemFont(ofSize: 15, weight: .bold)
                valueLabel.font = UIFont.systemFont(ofSize: 15, weight: .bold)
    
            } else {
    
                NSLayoutConstraint.activate([
                    keyLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 6.0),
                    keyLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4.0),
                    keyLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15.0),
    
                    valueLabel.centerYAnchor.constraint(equalTo: keyLabel.centerYAnchor, constant: 0.0),
                    valueLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15.0),
                    ])
    
                keyLabel.font = UIFont.systemFont(ofSize: 12, weight: .bold)
                valueLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
    
            }
    
        }
    
    }
    
    class TipCell: UITableViewCell {
    
        var callBack: (() -> ())?
    
        var theButton: UIButton = {
            let b = UIButton()
            b.translatesAutoresizingMaskIntoConstraints = false
            b.setTitle("Tap to Show/Hide Add Tip row", for: .normal)
            b.setTitleColor(.blue, for: .normal)
            b.backgroundColor = .yellow
            return b
        }()
    
        override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            commonInit()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
    
        func commonInit() -> Void {
    
            contentView.addSubview(theButton)
    
            NSLayoutConstraint.activate([
                theButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20.0),
                theButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20.0),
                theButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor, constant: 0.0),
                ])
    
            theButton.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
    
        }
    
        @objc func btnTapped(_ sender: Any?) -> Void {
            callBack?()
        }
    
    }
    
    class InsertRemoveViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
        var myData = [
            MyRowData(title: "SUBTOTAL", value: 4),
            MyRowData(title: "DELIVERY CHARGE", value: 1.99),
            MyRowData(title: "DISCOUNT", value: -1.99),
            MyRowData(title: "TOTAL", value: 4),
            ]
    
        var tableView: UITableView = {
            let v = UITableView()
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
    
    
        func tipRowShowHide() {
    
            let iPath = IndexPath(row: 3, section: 1)
    
            if myData.count == 4 {
                myData.insert(MyRowData(title: "DRIVER TIP", value: 2.0), at: 3)
                tableView.insertRows(at: [iPath], with: .automatic)
            } else {
                myData.remove(at: 3)
                tableView.deleteRows(at: [iPath], with: .automatic)
            }
    
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            tableView.register(OrderChargeTableViewCell.self,
                               forCellReuseIdentifier: OrderChargeTableViewCell.regularCellIdentifier)
            tableView.register(OrderChargeTableViewCell.self,
                               forCellReuseIdentifier: OrderChargeTableViewCell.boldCellIdentifier)
    
            tableView.register(TipCell.self, forCellReuseIdentifier: "TipCell")
    
            tableView.delegate = self
            tableView.dataSource = self
    
            tableView.rowHeight = UITableViewAutomaticDimension
            tableView.estimatedRowHeight = 25
    
            view.backgroundColor = .red
    
            view.addSubview(tableView)
    
            NSLayoutConstraint.activate([
                tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 200.0),
                tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20.0),
                tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20.0),
                tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20.0),
                ])
    
        }
    
        func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
            return " "
        }
    
        func numberOfSections(in tableView: UITableView) -> Int {
            return 2
        }
    
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return section == 0 ? 1 : myData.count
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
            if indexPath.section == 0 {
    
                let cell = tableView.dequeueReusableCell(withIdentifier: "TipCell", for: indexPath) as! TipCell
    
                cell.callBack = {
                    self.tipRowShowHide()
                }
    
                return cell
    
            }
    
            var cell: OrderChargeTableViewCell?
    
            if indexPath.row == myData.count - 1 {
    
                cell = tableView.dequeueReusableCell(withIdentifier: OrderChargeTableViewCell.boldCellIdentifier,
                                                     for: indexPath) as? OrderChargeTableViewCell
    
            } else {
    
                cell = tableView.dequeueReusableCell(withIdentifier: OrderChargeTableViewCell.regularCellIdentifier,
                                                     for: indexPath) as? OrderChargeTableViewCell
    
            }
    
            cell?.keyLabel.text = myData[indexPath.row].title
    
            let val = myData[indexPath.row].value
            cell?.valueLabel.text = String(format: "%0.02f USD", val)
    
            return cell!
    
        }
    
    }
    

    This is the result:

    enter image description here