Search code examples
iosswiftxcodeswift5xcode11

I have Run into a weird problem while making a basic tableview with automatic rowheight


My cell looks like this :

class listofJobsTVC: UITableViewCell {
    
    let jobName = UILabel()
    let jobNumber = UILabel()
    let jobDescription = UILabel()
    let jobStartDate = UILabel()
    let jobEndDate = UILabel()
    let jobTaskCount = UILabel()
    
    let expandBtn = UIButton()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        for view in contentView.subviews {
            view.removeFromSuperview()
        }
        SetupCell()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
        }
        
        func SetupCell() {
            let jobNameLeft = UILabel()
            let jobNumberLeft = UILabel()
            let jobDescriptionLeft = UILabel()
            let jobStartDateLeft = UILabel()
            let jobEndDateLeft = UILabel()
            let jobTaskCountLeft = UILabel()
            
            let container = ShadowRoundedView()
            
            addSubview(container)
            
            addConstraintsWithFormat("H:|-15-[v0]-15-|", views: container)
            addConstraintsWithFormat("V:|-15-[v0]-15-|", views: container)
            
            container.addSubview(jobName)
            container.addSubview(jobNumber)
            container.addSubview(jobDescription)
            container.addSubview(jobStartDate)
            container.addSubview(jobEndDate)
            container.addSubview(jobTaskCount)
            container.addSubview(jobNameLeft)
            container.addSubview(jobNumberLeft)
            container.addSubview(jobDescriptionLeft)
            container.addSubview(jobStartDateLeft)
            container.addSubview(jobEndDateLeft)
            container.addSubview(jobTaskCountLeft)
            container.addSubview(expandBtn)
            
            let leftWidth : CGFloat = 150
            let rightBtnWidth : CGFloat = 30
            
            container.addConstraintsWithFormat("V:|[v0]|", views: expandBtn)
            container.addConstraintsWithFormat("V:|-10-[v0]-8-[v1]-8-[v2]-8-[v3]-8-[v4]-8-[v5]-10-|", views: jobName, jobNumber, jobDescription, jobStartDate, jobEndDate, jobTaskCount)
            container.addConstraintsWithFormat("H:|-10-[v0(\(leftWidth))]-8-[v1]-10-[v2(\(rightBtnWidth))]|", views: jobNameLeft, jobName, expandBtn)
            container.addConstraintsWithFormat("H:|-10-[v0(\(leftWidth))]-8-[v1]-10-[v2(\(rightBtnWidth))]|", views: jobNumberLeft, jobNumber, expandBtn)
            container.addConstraintsWithFormat("H:|-10-[v0(\(leftWidth))]-8-[v1]-10-[v2(\(rightBtnWidth))]|", views: jobDescriptionLeft, jobDescription, expandBtn)
            container.addConstraintsWithFormat("H:|-10-[v0(\(leftWidth))]-8-[v1]-10-[v2(\(rightBtnWidth))]|", views: jobStartDateLeft, jobStartDate, expandBtn)
            container.addConstraintsWithFormat("H:|-10-[v0(\(leftWidth))]-8-[v1]-10-[v2(\(rightBtnWidth))]|", views: jobEndDateLeft, jobEndDate, expandBtn)
            container.addConstraintsWithFormat("H:|-10-[v0(\(leftWidth))]-8-[v1]-10-[v2(\(rightBtnWidth))]|", views: jobTaskCountLeft, jobTaskCount, expandBtn)
            jobNameLeft.topAnchor.constraint(equalTo: jobName.topAnchor).isActive = true
            jobNumberLeft.topAnchor.constraint(equalTo: jobNumber.topAnchor).isActive = true
            jobDescriptionLeft.topAnchor.constraint(equalTo: jobDescription.topAnchor).isActive = true
            jobStartDateLeft.topAnchor.constraint(equalTo: jobStartDate.topAnchor).isActive = true
            jobEndDateLeft.topAnchor.constraint(equalTo: jobEndDate.topAnchor).isActive = true
            jobTaskCountLeft.topAnchor.constraint(equalTo: jobTaskCount.topAnchor).isActive = true
            
            setUpLabel(AppColors.appPrimaryTealColor, jobName)
            setUpLabel(AppColors.appPrimaryTealColor, jobNumber)
            setUpLabel(AppColors.appPrimaryTealColor, jobDescription)
            setUpLabel(AppColors.appPrimaryTealColor, jobStartDate)
            setUpLabel(AppColors.appPrimaryTealColor, jobEndDate)
            setUpLabel(AppColors.appPrimaryTealColor, jobTaskCount)
            setUpLabel(.black, jobNameLeft)
            setUpLabel(.black, jobNumberLeft)
            setUpLabel(.black, jobDescriptionLeft)
            setUpLabel(.black, jobStartDateLeft)
            setUpLabel(.black, jobEndDateLeft)
            setUpLabel(.black, jobTaskCountLeft)
            jobNameLeft.text = "JOB NAME"
            jobNumberLeft.text = "JOB NUMBER"
            jobDescriptionLeft.text = "JOB DESCRIPTION"
            jobStartDateLeft.text = "JOB START DATE"
            jobEndDateLeft.text = "JOB END DATE"
            jobTaskCountLeft.text = "JOB TASK COUNT"
            expandBtn.roundCorners([.topRight,.bottomRight], radius: 15)
            expandBtn.backgroundColor = AppColors.appPrimaryTealColor
            expandBtn.setImage(#imageLiteral(resourceName: "arrow-down-sign-to-navigate").maskWithColor(color: .white), for: .normal)
            expandBtn.imageView?.contentMode = .scaleAspectFit
            expandBtn.imageEdgeInsets = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)
            container.cornerRadious = 15
        }
        
        func setUpLabel(_ color : UIColor, _ label : UILabel){
            label.font = UIFont.systemFont(ofSize: 16, weight: .semibold)
            label.numberOfLines = 0
            label.textColor = color
        }
        
    }

and in my view controller I am making my table have automatic dimension like this :

mainTable.backgroundColor = .clear
        mainTable.separatorStyle = .none
        mainTable.rowHeight = UITableView.automaticDimension
        mainTable.dataSource = self
        mainTable.delegate = self

and here is the cell for row method :

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = listofJobsTVC()
        cell.jobName.text = ": 1"
        cell.jobNumber.text = ": 1"
        cell.jobDescription.text = ": 1"
        cell.jobStartDate.text = ": 1"
        cell.jobEndDate.text = ": 1"
        cell.jobTaskCount.text = ": 1"
        return cell
    }

the design of the cell comes out perfect but this weird constraint keeps breaking for each cell I create which is really annoying

2021-02-21 14:56:30.961185+0530 INDORE IPDS[30128:647175] SQLiteDB opened!
2021-02-21 14:56:34.325851+0530 INDORE IPDS[30128:647175] [LayoutConstraints] 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. 
(
    "<NSLayoutConstraint:0x600001363890 V:|-(15)-[INDORE_IPDS.ShadowRoundedView:0x7f806ec2a140]   (active, names: '|':INDORE_IPDS.listofJobsTVC:0x7f806ec27880 )>",
    "<NSLayoutConstraint:0x600001363840 V:[INDORE_IPDS.ShadowRoundedView:0x7f806ec2a140]-(15)-|   (active, names: '|':INDORE_IPDS.listofJobsTVC:0x7f806ec27880 )>",
    "<NSLayoutConstraint:0x600001363700 V:|-(10)-[UILabel:0x7f806ec10e60]   (active, names: '|':INDORE_IPDS.ShadowRoundedView:0x7f806ec2a140 )>",
    "<NSLayoutConstraint:0x6000013636b0 V:[UILabel:0x7f806ec10e60]-(8)-[UILabel:0x7f806ec27c50]   (active)>",
    "<NSLayoutConstraint:0x600001363660 V:[UILabel:0x7f806ec27c50]-(8)-[UILabel:0x7f806ec27ed0]   (active)>",
    "<NSLayoutConstraint:0x600001363610 V:[UILabel:0x7f806ec27ed0]-(8)-[UILabel:0x7f806ec28150]   (active)>",
    "<NSLayoutConstraint:0x6000013635c0 V:[UILabel:0x7f806ec28150]-(8)-[UILabel:0x7f806ec283d0]   (active)>",
    "<NSLayoutConstraint:0x600001363570 V:[UILabel:0x7f806ec283d0]-(8)-[UILabel:0x7f806ec28650]   (active)>",
    "<NSLayoutConstraint:0x600001363520 V:[UILabel:0x7f806ec28650]-(1000)-|   (active, names: '|':INDORE_IPDS.ShadowRoundedView:0x7f806ec2a140 )>",
    "<NSLayoutConstraint:0x6000013793b0 'UIView-Encapsulated-Layout-Height' INDORE_IPDS.listofJobsTVC:0x7f806ec27880.height == 44   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600001363570 V:[UILabel:0x7f806ec283d0]-(8)-[UILabel:0x7f806ec28650]   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

I have made cells this way a thousand times but never saw constraints break like this. I am guessing the 44 is default height of a Tableview cell. Also don't tell me to put estimated row height.. done that.. did not change anything also all the cells I created before works fine without estimated row height.. I dunno what to do .. I don't see any wrong constraints in the cell either..

For reference heres the addConstraintsWithFormat method :

func addConstraintsWithFormat(_ format: String, views: UIView...) {
        var viewsDictionary = [String: UIView]()
        for (index, view) in views.enumerated() {
            let key = "v\(index)"
            view.translatesAutoresizingMaskIntoConstraints = false
            viewsDictionary[key] = view
        }
        addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: [], metrics: nil, views: viewsDictionary))
    }

Solution

  • This is a common issue...

    As I understand it, auto-layout begins its process with the assumption that a row (cell) has the default height of 44. As it starts laying out the cell's UI elements, it can throw warnings because it has not yet finished the layout, and at that point there are conflicts.

    That's why you get the warning / error messages, but the cell lays out as expected.

    One way around this is to give the Bottom constraint a Priority of less-than-required.

    You can do this by changing one line in your code from:

    container.addConstraintsWithFormat("V:|-10-[v0]-8-[v1]-8-[v2]-8-[v3]-8-[v4]-8-[v5]-10-|", views: jobName, jobNumber, jobDescription, jobStartDate, jobEndDate, jobTaskCount)
    

    to:

    container.addConstraintsWithFormat("V:|-10-[v0]-8-[v1]-8-[v2]-8-[v3]-8-[v4]-8-[v5]-10@750-|", views: jobName, jobNumber, jobDescription, jobStartDate, jobEndDate, jobTaskCount)
    

    Note the addition of "[v5]-10 @750 -|" for the last vertical constraint.

    That allows auto-layout to "temporarily" break the constraint without filling the debug console with warning messages.


    As a side note, it's my impression that VFL has been dropped by Apple. At least, I haven't seen anything new about it for several years. While it can work fine for simple layouts, it has many deficiencies and, in my view, is not nearly as clear or easy to debug when problems arise.

    In addition, the way you've done this ends up applying both the Width and Trailing constraints to your expandBtn 6 times. While it has no adverse effect in this specific instance, it's pretty easy to accidentally add multiple constraints which end up having conflicts.

    Here's another approach that I, personally, find a bit more logical and much easier to manage and debug:

        let leftWidth : CGFloat = 150
        let rightBtnWidth : CGFloat = 30
        
        expandBtn.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            // constrain expand button
            // Top / Trailing / Bottom to container
            expandBtn.topAnchor.constraint(equalTo: container.topAnchor, constant: 0.0),
            expandBtn.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),
            expandBtn.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 0.0),
            // width = rightBtnWidth (constant value)
            expandBtn.widthAnchor.constraint(equalToConstant: rightBtnWidth),
        ])
        
        // arrays of left and right labels
        let leftLabels: [UILabel] = [
            jobNameLeft, jobNumberLeft, jobDescriptionLeft, jobStartDateLeft, jobEndDateLeft, jobTaskCountLeft,
        ]
        let rightLabels: [UILabel] = [
            jobName, jobNumber, jobDescription, jobStartDate, jobEndDate, jobTaskCount,
        ]
        
        // this will be tracked inside the for loop
        var prevLabel: UILabel!
        
        for (leftLabel, rightLabel) in zip(leftLabels, rightLabels) {
            
            // of course
            leftLabel.translatesAutoresizingMaskIntoConstraints = false
            rightLabel.translatesAutoresizingMaskIntoConstraints = false
            
            NSLayoutConstraint.activate([
                // all left labels have Leading: 10.0 and Width: leftWidth
                leftLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 10.0),
                leftLabel.widthAnchor.constraint(equalToConstant: leftWidth),
                // all right labels have Leading: 8.0 from left label Trailing
                rightLabel.leadingAnchor.constraint(equalTo: leftLabel.trailingAnchor, constant: 8.0),
                // all right labels have Trailing: -10.0 from expand button Leading
                rightLabel.trailingAnchor.constraint(equalTo: expandBtn.leadingAnchor, constant: -10.0),
                // all right labels have Top equal to respective left label Top
                rightLabel.topAnchor.constraint(equalTo: leftLabel.topAnchor, constant: 0.0),
            ])
            
            if nil == prevLabel {
                // if it's the First left label
                //  constrain Top to container Top + 10.0
                leftLabel.topAnchor.constraint(equalTo: container.topAnchor, constant: 10.0).isActive = true
            } else {
                // not the first label, so
                //  constrain Top to previous label Bottom + 8.0
                leftLabel.topAnchor.constraint(equalTo: prevLabel.bottomAnchor, constant: 8.0).isActive = true
            }
            
            if leftLabel == leftLabels.last {
                // if it's the Last left label
                //  constrain Bottom to container Bottom - 10.0
                let c = leftLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -10.0)
                // also give it a less-than-required Priority, so auto-layout won't complain
                c.priority = .defaultHigh
                c.isActive = true
            }
            
            prevLabel = leftLabel
        }
    
        // this is no longer needed
        /*
        container.addConstraintsWithFormat("V:|[v0]|", views: expandBtn)
        container.addConstraintsWithFormat("V:|-10-[v0]-8-[v1]-8-[v2]-8-[v3]-8-[v4]-8-[v5]-10@750-|", views: jobName, jobNumber, jobDescription, jobStartDate, jobEndDate, jobTaskCount)
        container.addConstraintsWithFormat("H:|-10-[v0(\(leftWidth))]-8-[v1]-10-[v2(\(rightBtnWidth))]|", views: jobNameLeft, jobName, expandBtn)
        container.addConstraintsWithFormat("H:|-10-[v0(\(leftWidth))]-8-[v1]-10-[v2(\(rightBtnWidth))]|", views: jobNumberLeft, jobNumber, expandBtn)
        container.addConstraintsWithFormat("H:|-10-[v0(\(leftWidth))]-8-[v1]-10-[v2(\(rightBtnWidth))]|", views: jobDescriptionLeft, jobDescription, expandBtn)
        container.addConstraintsWithFormat("H:|-10-[v0(\(leftWidth))]-8-[v1]-10-[v2(\(rightBtnWidth))]|", views: jobStartDateLeft, jobStartDate, expandBtn)
        container.addConstraintsWithFormat("H:|-10-[v0(\(leftWidth))]-8-[v1]-10-[v2(\(rightBtnWidth))]|", views: jobEndDateLeft, jobEndDate, expandBtn)
        container.addConstraintsWithFormat("H:|-10-[v0(\(leftWidth))]-8-[v1]-10-[v2(\(rightBtnWidth))]|", views: jobTaskCountLeft, jobTaskCount, expandBtn)
        
        jobNameLeft.topAnchor.constraint(equalTo: jobName.topAnchor).isActive = true
        jobNumberLeft.topAnchor.constraint(equalTo: jobNumber.topAnchor).isActive = true
        jobDescriptionLeft.topAnchor.constraint(equalTo: jobDescription.topAnchor).isActive = true
        jobStartDateLeft.topAnchor.constraint(equalTo: jobStartDate.topAnchor).isActive = true
        jobEndDateLeft.topAnchor.constraint(equalTo: jobEndDate.topAnchor).isActive = true
        jobTaskCountLeft.topAnchor.constraint(equalTo: jobTaskCount.topAnchor).isActive = true
        */