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
}
}
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)
}