2

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

abbood
  • 23,101
  • 16
  • 132
  • 246
  • Can you reduce your code to a bare minimum that demonstrates the issue? I just did a quick test and have no problem inserting / removing a row while auto-layout handles the row heights. – DonMag Dec 04 '18 at 21:25
  • hey abbood, when i have such problems i try to reduce the complexity to a minimum, means remove setting the font, stay with on label that is aligned to the content with with a fox margin. than you can set the contraints already in the init method, than you dont need to store this didSrtupConstraint thing, when you have the minimum stiff, just try again. Does the Xcode console say sonething? breaking constraints or so? – Björn Ro Dec 04 '18 at 22:16
  • ahhh and try to setup the cell in the awakeFromNib function, just to see if it helps :) – Björn Ro Dec 04 '18 at 22:17
  • @BjörnRo as a matter of fact i need to set the `didSrtupConstraint` thing, it's to ensure that i don't apply the constraints several times. from [this SO answer](https://stackoverflow.com/a/18746930/766570): `If you're adding constraints in code, you should do this once from within the updateConstraints method of your UITableViewCell subclass. Note that updateConstraints may be called more than once, so to avoid adding the same constraints more than once,..` – abbood Dec 05 '18 at 06:23
  • `.. make sure to wrap your constraint-adding code within updateConstraints in a check for a boolean property such as didSetupConstraints (which you set to YES after you run your constraint-adding code once). On the other hand, if you have code that updates existing constraints (such as adjusting the constant property on some constraints), place this in updateConstraints but outside of the check for didSetupConstraints so it can run every time the method is called.` – abbood Dec 05 '18 at 06:23
  • also @BjörnRo i'm not constructing a cell from a .xib file, so that awakenFromNib callback won't do me no good – abbood Dec 05 '18 at 06:41
  • @DonMag I really believed i shared only the parts of the code that needs to be shared. Again I'm using the same layout paradigm as instructed by pure layout (you can see an example of such a cell [here](https://github.com/PureLayout/PureLayout/blob/master/PureLayout/Example-iOS/Demos/ALiOSDemo1ViewController.m)).. maybe your quick test is different than their style – abbood Dec 05 '18 at 06:43

3 Answers3

1

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

DonMag
  • 69,424
  • 5
  • 50
  • 86
0

this is a brute force solution, one that i'm not proud of at all, but it's here for reference (won't mark it as correct answer):

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    let orderChargesSection = self.getOrderChargesSection()
    switch indexPath.section {
        case orderChargesSection:
            return self.getCellHeightForOrderCharges(row: indexPath.row)
        default:
            return UITableViewAutomaticDimension
    }
}

private func getCellHeightForOrderCharges(row: Int) -> CGFloat {
   let numRows = self.tableView(self.tableView, numberOfRowsInSection: self.getOrderChargesSection())
    if (row == numRows - 1) {
        return UITableViewAutomaticDimension
    } else {
        return 25.5
    }
}
abbood
  • 23,101
  • 16
  • 132
  • 246
0

After beginUpdates / endUpdate and all of the inserts do this:

DispatchQueue.main.async {
    self.tableView.beginUpdates()
    self.tableView.endUpdates()
}
Maciej Stramski
  • 210
  • 2
  • 11