Search code examples
uitableviewnslayoutconstraint

How to set width of a UITableView equal to the maximum cells' width inside of it?


I have a table view and I using a custom cell which has 3 UI elements as subviews. I have made my UIelements which are labels to shrink as per there content size. Now my problem is to set cell to shrink as per its UIElements and relatively adjust tableview width.enter image description here


Solution

  • One way could be to:

    • create a separate view e.g. named ThreeElementView which will be added to the content view of the cell
    • with all rows available you could call systemLayoutSizeFitting to get the maximum width
    • add an width constraint (NSLayoutConstraint) to the table view
    • if the data of the table view changes, adjust the constraint

    The widthContraint can be setup like this:

    private var widthContraint: NSLayoutConstraint?
    
    widthContraint = tableView.widthAnchor.constraint(equalToConstant: 128)
    widthContraint?.isActive = true
    if let width = calcWidth() {
        widthContraint?.constant = width
    }
    

    You would also call the last 3 lines before updating the table view with tableView.reloadData().

    Assuming that data contains the actual table data, the width calculation could look like this:

    private func calcWidth() -> CGFloat? {
        let prototypeView = ThreeElementView()
        let widths = data.map { row -> CGFloat in
            prototypeView.label1.text = row[0]
            prototypeView.label2.text = row[1]
            prototypeView.label3.text = row[2]
            prototypeView.setNeedsLayout()
            prototypeView.layoutIfNeeded()
            return prototypeView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width
        }
        return widths.max()
    }
    

    So for each line you would calculate the width of the contents and finally return the maximum value.

    Self-Contained Test

    Here is a self-contained test of the above. The UI has been built programmatically in code so that the result is easier to follow. If you press the button, you can see that the width of the tableview then also dynamically adjusts just by setting the constraint.

    ThreeElementView.swift

    import UIKit
    
    class ThreeElementView: UIView {
        
        let label1 = UILabel()
        let label2 = UILabel()
        let label3 = UILabel()
        
        init() {
            super.init(frame: .zero)
            
            label1.backgroundColor = UIColor(red: 84/255, green: 73/255, blue: 75/255, alpha: 1.0)
            label1.textColor = .white
            label2.backgroundColor = UIColor(red: 131/255, green: 151/255, blue: 136/255, alpha: 1.0)
            label2.textColor = .white
            label3.backgroundColor = UIColor(red: 189/255, green: 187/255, blue: 182/255, alpha: 1.0)
            
            label1.translatesAutoresizingMaskIntoConstraints = false
            label2.translatesAutoresizingMaskIntoConstraints = false
            label3.translatesAutoresizingMaskIntoConstraints = false
            
            self.addSubview(label1)
            self.addSubview(label2)
            self.addSubview(label3)
            
            NSLayoutConstraint.activate([
                label1.leadingAnchor.constraint(equalTo: self.leadingAnchor),
                label1.topAnchor.constraint(equalTo: self.topAnchor),
                label1.bottomAnchor.constraint(equalTo: self.bottomAnchor),
                
                label2.leadingAnchor.constraint(equalTo: label1.trailingAnchor),
                label2.topAnchor.constraint(equalTo: self.topAnchor),
                label2.bottomAnchor.constraint(equalTo: self.bottomAnchor),
                
                label3.leadingAnchor.constraint(equalTo: label2.trailingAnchor),
                label3.topAnchor.constraint(equalTo: self.topAnchor),
                label3.bottomAnchor.constraint(equalTo: self.bottomAnchor),
                label3.trailingAnchor.constraint(equalTo: self.trailingAnchor)
            ])
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
    }
    

    ThreeElementCell.swift

    import UIKit
    
    class ThreeElementCell: UITableViewCell {
        
        static let id = "ThreeElementCellId"
        let threeElementView = ThreeElementView()
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            
            threeElementView.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(threeElementView)
            NSLayoutConstraint.activate([
                threeElementView.topAnchor.constraint(equalTo: contentView.topAnchor),
                threeElementView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
                threeElementView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
                threeElementView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
            ])
        }
        
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
    }
    

    ViewController.swift

    import UIKit
    
    class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
        
        private let tableView = UITableView()
        private let addMoreButton = UIButton()
        private var data = [
            ["a", "tiny", "row"],
        ]
        private var widthContraint: NSLayoutConstraint?
    
        override func viewDidLoad() {
            super.viewDidLoad()
            setupTableView()
            setupButton()
        }
        
        @objc func onAddMore() {
            if data.count < 2 {
                data.append(["a", "little bit", "longer row"])
            } else {
                data.append(["this is", " finally an even longer", "row"])
            }
            if let width = calcWidth() {
                widthContraint?.constant = width
            }
            tableView.reloadData()
        }
        
        // MARK: - UITableViewDataSource
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return data.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: ThreeElementCell.id, for: indexPath) as! ThreeElementCell
            let item = data[indexPath.row]
            cell.threeElementView.label1.text = item[0]
            cell.threeElementView.label2.text = item[1]
            cell.threeElementView.label3.text = item[2]        
            return cell
        }
        
        // MARK: - Private
        
        private func setupTableView() {
            tableView.backgroundColor = UIColor(red: 245/255, green: 228/255, blue: 215/255, alpha: 1.0)
            tableView.register(ThreeElementCell.self, forCellReuseIdentifier: ThreeElementCell.id)
            tableView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(tableView)
    
            NSLayoutConstraint.activate([
                tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16.0),
                tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16.0),
            ])
            widthContraint = tableView.widthAnchor.constraint(equalToConstant: 128)
            
            widthContraint?.isActive = true
            if let width = calcWidth() {
                widthContraint?.constant = width
            }
            
            tableView.delegate = self
            tableView.dataSource = self
        }
        
        private func setupButton() {
            addMoreButton.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(addMoreButton)
            NSLayoutConstraint.activate([
                addMoreButton.topAnchor.constraint(equalTo: tableView.bottomAnchor, constant: 32.0),
                addMoreButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                addMoreButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32.0),
            ])
            addMoreButton.setTitle("Add More Rows", for: .normal)
            addMoreButton.setTitleColor(.blue, for: .normal)
            addMoreButton.addTarget(self, action: #selector(onAddMore), for: .touchUpInside)
        }
    
        private func calcWidth() -> CGFloat? {
            let prototypeView = ThreeElementView()
            let widths = data.map { row -> CGFloat in
                prototypeView.label1.text = row[0]
                prototypeView.label2.text = row[1]
                prototypeView.label3.text = row[2]
                prototypeView.setNeedsLayout()
                prototypeView.layoutIfNeeded()
                return prototypeView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width
            }
            return widths.max()
        }
        
    }
    

    Demo

    demo