Search code examples
iosswiftuitableviewtableview

Why don't my column separators line up in my table view in my Swift Xcode project?


I have created custom header and custom table view cell classes for my custom table view but the column separators don't line up exactly even though I have the fields the same width in the header and cells. I've noticed that the cell width is 320, while the header width and the table view width are both 311.

Here is my custom table view cell class:

class LOMCell: UITableViewCell {

    // MARK: - Properties
    
    lazy var nameLabel: UILabel = {
        let label = UILabel()
        label.textColor = .black
        label.textAlignment = .center
        label.font = UIFont(name: "AvenirNext-Regular", size: 12.0)
        label.backgroundColor = .white
        label.layer.borderWidth = 0.25
        label.layer.borderColor = UIColor.black.cgColor
        label.setWidth(width: 4.0 * self.frame.width / 16.0)
        return label
    }()

    private lazy var containerView: UIView = {
        let cv = UIView()
        cv.addSubview(ratingImageView)
        ratingImageView.anchor(top: cv.topAnchor, left: cv.leftAnchor, bottom: cv.bottomAnchor, right: cv.rightAnchor, paddingTop: 0.0, paddingLeft: 0.0, paddingBottom: 0.0, paddingRight: 0.0)
        cv.layer.borderWidth = 0.25
        cv.layer.borderColor = UIColor.black.cgColor
        cv.setWidth(width: 5.0 * self.frame.width / 16.0 )
        return cv
    }()
    
    lazy var ratingImageView: UIImageView = {
        let iv = UIImageView()
        iv.backgroundColor = .white
        iv.contentMode = .scaleAspectFit
        iv.clipsToBounds = true
        return iv
    }()
    
    lazy var footprintLabel: UILabel = {
        let label = UILabel()
        label.textColor = .black
        label.textAlignment = .center
        label.font = UIFont(name: "AvenirNext-Regular", size: 12.0)
        label.backgroundColor = .white
        label.layer.borderWidth = 0.25
        label.layer.borderColor = UIColor.black.cgColor
        label.setWidth(width: 3.5 * self.frame.width / 16.0)
        return label
    }()
    
    lazy var feedbackLabel: UILabel = {
        let label = UILabel()
        label.textAlignment = .center
        label.font = UIFont(name: "AvenirNext-Regular", size: 12.0)
        label.textColor = .black
        label.backgroundColor = .white
        label.layer.borderWidth = 0.25
        label.layer.borderColor = UIColor.black.cgColor
        label.setWidth(width: 3.5 * self.frame.width / 16.0)
        return label
    }()
    
    // MARK: - Lifecycle
    
    override init(style: UITableViewCell.CellStyle , reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        configureUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Helper Functions
    
    private func configureUI() {
        
        let stackView = UIStackView(arrangedSubviews: [nameLabel,
                                                       containerView,
                                                       footprintLabel,
                                                       feedbackLabel])
        stackView.axis = .horizontal
        stackView.distribution = .fill
        stackView.spacing = 0
        
        self.addSubview(stackView)
        stackView.anchor(top: self.topAnchor,
                         left: self.leftAnchor,
                         bottom: self.bottomAnchor,
                         right: self.rightAnchor,
                         paddingTop: 0.0,
                         paddingLeft: 0.0,
                         paddingBottom: 0.0,
                         paddingRight: 0.0)
        
        print("DEBUG: cell width = \(self.frame.width)")
    }
}

Here is my custom table view header class:

class LOMHeader: UIView {
    
    // MARK: - Properties
    
    private lazy var nameLabel: UILabel = {
        let label = UILabel()
        label.text = "Name"
        label.textColor = .white
        label.textAlignment = .center
        label.backgroundColor = .systemGreen
        label.font = UIFont(name: "AvenirNext-Bold", size: 12.0)
        label.layer.borderWidth = 0.25
        label.layer.borderColor = UIColor.black.cgColor
        label.setWidth(width: 4.0 * self.frame.width / 16.0)
        return label
    }()

    private lazy var ratingLabel: UILabel = {
        let label = UILabel()
        label.text = "Rating"
        label.textColor = .white
        label.textAlignment = .center
        label.backgroundColor = .systemGreen
        label.font = UIFont(name: "AvenirNext-Bold", size: 12.0)
        label.layer.borderWidth = 0.25
        label.layer.borderColor = UIColor.black.cgColor
        label.setWidth(width: 5.0 * self.frame.width / 16.0)
        return label
    }()

    private lazy var footprintLabel: UILabel = {
        let label = UILabel()
        label.text = "Footprint"
        label.textColor = .white
        label.textAlignment = .center
        label.backgroundColor = .systemGreen
        label.font = UIFont(name: "AvenirNext-Bold", size: 12.0)
        label.layer.borderWidth = 0.25
        label.layer.borderColor = UIColor.black.cgColor
        label.setWidth(width: 3.5 * self.frame.width / 16.0)
        return label
    }()

    private lazy var feedbackLabel: UILabel = {
        let label = UILabel()
        label.text = "Feedback"
        label.textColor = .white
        label.textAlignment = .center
        label.backgroundColor = .systemGreen
        label.font = UIFont(name: "AvenirNext-Bold", size: 12.0)
        label.layer.borderWidth = 0.25
        label.layer.borderColor = UIColor.black.cgColor
        label.setWidth(width: 3.5 * self.frame.width / 16.0)
        return label
    }()

    // MARK: - Lifecycle
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        let stackView = UIStackView(arrangedSubviews: [nameLabel,
                                                       ratingLabel,
                                                       footprintLabel,
                                                       feedbackLabel])
        stackView.axis = .horizontal
        stackView.distribution = .fill
        stackView.spacing = 0
        
        self.addSubview(stackView)
        stackView.anchor(top: topAnchor,
                           left: leftAnchor,
                           bottom: bottomAnchor,
                           right: rightAnchor,
                           paddingTop: 0.0,
                           paddingLeft: 0.0,
                           paddingBottom: 0.0,
                           paddingRight: 0.0)
        
        print("DEBUG: header width = \(self.frame.width)")
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

Here is my custom view controller with custom table view:

class ListOfMembersVC: UIViewController {

    // MARK: - Properties
    
    private let cellID = "ListOfMembersCellID"
    
    private lazy var tableView: UITableView = {
        let tv = UITableView()
        tv.rowHeight = 40.0
        tv.register(LOMCell.self, forCellReuseIdentifier: cellID)
        tv.delegate = self
        tv.dataSource = self
        return tv
    }()
    
    private let maxNumberOfRows = 6
    
    private let listOfMembers: [[String : Any]] = [["Name":"Louise", "Rating":UIImage(imageLiteralResourceName: "Rating Stars 2 out of 5"), "Footprint": 2, "Feedback": "??"]]
    
    // MARK: - Lifecycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureUI()
        
    }
    
    // MARK: - Helper Functions
    
    private func configureUI() {
        
        configureGradientLayer()
        
        title = "List of Members"

        navigationController?.navigationBar.barTintColor = .systemBlue
        navigationController?.navigationBar.tintColor = .white
        navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 30)]
        navigationController?.navigationBar.barStyle = .black
        
        view.addSubview(tableView)
        tableView.anchor(top: view.safeAreaLayoutGuide.topAnchor,
                         left: view.leftAnchor,
                         right: view.rightAnchor,
                         paddingTop: 40.0,
                         paddingLeft: 32.0,
                         paddingRight: 32.0,
                         height: 60.0 + 40.8 * CGFloat(maxNumberOfRows))
        
    }

}

extension ListOfMembersVC: UITableViewDelegate {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 60.0
    }
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let header = LOMHeader(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 60.0))
        print("DEBUG: table width = \(self.tableView.frame.width)")
        return header
    }
}

extension ListOfMembersVC: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return listOfMembers.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! LOMCell
        cell.nameLabel.text = listOfMembers[indexPath.row]["Name"] as? String
        cell.ratingImageView.image = (listOfMembers[indexPath.row]["Rating"] as? UIImage)?.withAlignmentRectInsets(UIEdgeInsets(top: -5, left: -10, bottom: -5, right: -10))
        cell.footprintLabel.text = String((listOfMembers[indexPath.row]["Footprint"] as? Int)!)
        cell.feedbackLabel.text = listOfMembers[indexPath.row]["Feedback"] as? String
        print("DEBUG: cell width 2 = \(cell.frame.width)")
        return cell
    }
    

}

And here is a screenshot of the problem: Column separators not lining up


Solution

  • First of all you aren't going to get valid frame sizes until your views are laid out. You'll get more accurate frame sizes in viewDidAppear:

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        print("INFO: tableView.width: \(tableView.frame.size.width)")
        tableView.visibleCells.compactMap({ $0 as? LOMCell }).forEach {
            print("INFO: tableView.cell.width: \($0.frame.size.width)")
        }
        tableView.subviews.compactMap { $0 as? LOMHeader }.forEach {
            print("INFO: tableView.header.width: \($0.frame.size.width)")
        }
    }
    

    You have the same problem when you try to constrain your subview widths like this. You need to get rid of these.

    label.setWidth(width: 4.0 * self.frame.width / 16.0)
    

    The cell's frame property isn't valid until it get's laid out.

    What I would do is make the width proportional to its superview to give you more flexibility when the cell or header size changes (such as during rotation):

    extension UIView {
        func setWidthProportionalToSuperview(by multipler: CGFloat) {
            guard let superview = superview else { fatalError("Missing superview") }
            widthAnchor.constraint(equalTo: superview.widthAnchor, multiplier: multipler).isActive = true
        }
    }
    

    You can set up the proportions as soon as you add the subviews to the stackView:

    private func configureUI() {
        
        let stackView = UIStackView(arrangedSubviews: [nameLabel,
                                                       containerView,
                                                       footprintLabel,
                                                       feedbackLabel])
        stackView.axis = .horizontal
        stackView.distribution = .fill
        stackView.spacing = 0
        
        self.addSubview(stackView)
        stackView.anchor(top: self.topAnchor,
                         left: self.leftAnchor,
                         bottom: self.bottomAnchor,
                         right: self.rightAnchor,
                         paddingTop: 0.0,
                         paddingLeft: 0.0,
                         paddingBottom: 0.0,
                         paddingRight: 0.0)
    
        nameLabel.setWidthProportionalToSuperview(by: 4.0 / 16.0)
        containerView.setWidthProportionalToSuperview(by: 5.0 / 16.0)
        footprintLabel.setWidthProportionalToSuperview(by: 3.5 / 16.0)
        feedbackLabel.setWidthProportionalToSuperview(by: 3.5 / 16.0)
    
    }
    

    Lastly, you shouldn't add subviews directly to your cell. You should add then as a subview of your contentView:

    private func configureUI() {
        
        let stackView = UIStackView(arrangedSubviews: [nameLabel,
                                                       containerView,
                                                       footprintLabel,
                                                       feedbackLabel])
        // ...
    
        contentView.addSubview(stackView)
        stackView.anchor(top: contentView.topAnchor,
                         left: contentView.leftAnchor,
                         bottom: contentView.bottomAnchor,
                         right: contentView.rightAnchor,
                         paddingTop: 0.0,
                         paddingLeft: 0.0,
                         paddingBottom: 0.0,
                         paddingRight: 0.0)
    }