Search code examples
iosswiftuitableviewuistackviewsnapkit

How should I set constraints to the subviews of my tableHeaderView?


I have this viewController:

class CreateSkillGroupViewController: UIViewController {
    
    lazy var headerStack: UIStackView = {
        let stack = UIStackView(frame: CGRect(x: 0, y: 0, width: 20, height: 400))
        stack.axis = .vertical
        let titleField = UITextView(frame: CGRect(x: 0, y: 0, width: 300, height: 88))
        titleField.backgroundColor = .green
        titleField.snp.makeConstraints{ (make) in
            make.height.equalTo(50)
        }
        let descriptionField = UITextView(frame: CGRect(x: 0, y: 0, width: 300, height: 120))
        descriptionField.snp.makeConstraints{ (make) in
            make.height.equalTo(100)
        }
        let headerImage = UIImageView(image: UIImage(named: "AppIcon-bw"))
        headerImage.snp.makeConstraints{ (make) in
            make.height.equalTo(300)
            make.width.equalTo(200)
        }
        stack.addArrangedSubview(headerImage)
        stack.addArrangedSubview(titleField)
        stack.addArrangedSubview(descriptionField)
        stack.backgroundColor = .blue
        return stack
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureNavigationItem()

        skillsTableView = UITableView(frame: .zero, style: .insetGrouped)
        skillsTableView.register(SkillSummaryCell.self)
        skillsTableView.tableHeaderView = headerStack
        view.addSubview(skillsTableView)
        skillsTableView.tableHeaderView?.snp.makeConstraints{ (make) in
            make.top.equalToSuperview()
            make.left.equalToSuperview()
            make.right.equalToSuperview()
            make.width.equalToSuperview()
            make.height.equalTo(400)
        }
        skillsTableView.snp.makeConstraints{ (make) in
            make.edges.equalToSuperview()
        }
   ...

This is what it creates...

enter image description here

As you can see I use the lazy var headerStack to setup the tableHeaderView which is a stackView. As you can see all of the constraints in that stack view are explicit number sizes. Then in the viewDidLoad, I add the constraints for the tableView itself.

I want to know how I would for instance, center the headerImage in the viewController, or in the tableView for that matter or make its width half of the tableView's width. I cannot set equalToSuperView because the view hasn't been laid out yet. And once its laid out, I cannot access the stack view subviews to retroactively add constraints to them.


Solution

  • First of all, I wouldn't use a stackView as a tableHeaderView because you need your tableHeaderView to be the same width as the tableView. Embed your stackView in a view and use that view as the header. Ensure that header remains the width of the tableView regardless of the stackView content.

    Also, it looks like you are trying to mix autolayout with frame-based layout and that's gonna get you into trouble. I'm not sure why you were setting frames on some of your subviews.

    Pay attention to how you define stackView.alignment and stackView.distribution. I'm not sure what your goal is so it's hard to give you much advice there. Bit I assume you want your subviews centered and to have their own unique width.

    You defined a lot of your subviews in your stackView builder and that got you into trouble. Ensure that you have one builder for each subview. It helps keep your code clean.

    Lastly, you can use autolayout to define the width equal to the width of the tableView. There are a lot of solutions on the web that make you compute the frames for your header manually and that's just a pain.

    I changed some names around added some colors but I think this will help you:

    extension UIColor {
        static let headerImage = UIColor.systemPurple
        static let header = UIColor.systemPink
        static let titleField = UIColor.white
        static let descriptionField = UIColor.systemYellow
        static let headerStack = UIColor.systemOrange
        static let tableView = UIColor.systemMint
    }
    
    class ViewController: UIViewController {
    
        lazy var headerImage: UIImageView = {
            let headerImage = UIImageView(image: UIImage(systemName: "checkmark"))
            headerImage.translatesAutoresizingMaskIntoConstraints = false
            headerImage.backgroundColor = .headerImage
            return headerImage
        }()
    
        lazy var headerView: UIView = {
            let header = UIView()
            header.backgroundColor = .header
            header.translatesAutoresizingMaskIntoConstraints = false
            return header
        }()
    
        lazy var titleField: UITextView = {
            let titleField = UITextView(frame: .zero)
            titleField.translatesAutoresizingMaskIntoConstraints = false
            titleField.backgroundColor = .titleField
            return titleField
        }()
    
        lazy var descriptionField: UITextView = {
            let descriptionField = UITextView(frame: .zero)
            descriptionField.translatesAutoresizingMaskIntoConstraints = false
            descriptionField.backgroundColor = .descriptionField
            return descriptionField
        }()
    
        lazy var headerStack: UIStackView = {
            let stack = UIStackView(frame: .zero)
            stack.translatesAutoresizingMaskIntoConstraints = false
            stack.axis = .vertical
            stack.distribution = .fillProportionally
            stack.alignment = .center
            stack.spacing = 10
            stack.backgroundColor = .headerStack
            return stack
        }()
    
        lazy var tableView: UITableView = {
            let tableView = UITableView(frame: .zero, style: .insetGrouped)
            tableView.translatesAutoresizingMaskIntoConstraints = false
            tableView.register(SkillSummaryCell.self, forCellReuseIdentifier: "SkillSummaryCell")
            tableView.backgroundColor = .tableView
            tableView.delegate = self
            tableView.dataSource = self
            return tableView
        }()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            addViews()
            arrangeViews()
            tableView.layoutIfNeeded()
        }
    
        func addViews() {
            view.addSubview(tableView)
    
            headerStack.addArrangedSubview(headerImage)
            headerStack.addArrangedSubview(titleField)
            headerStack.addArrangedSubview(descriptionField)
    
            headerView.addSubview(headerStack)
    
            tableView.tableHeaderView = headerView
        }
    
        func arrangeViews() {
            tableView.snp.makeConstraints{ (make) in
                make.edges.equalTo(view.safeAreaLayoutGuide)
            }
    
            descriptionField.snp.makeConstraints{ (make) in
                make.height.equalTo(100)
                make.width.equalTo(300)
            }
    
            titleField.snp.makeConstraints{ (make) in
                make.height.equalTo(100)
                make.width.equalTo(300)
            }
    
            headerStack.snp.makeConstraints { make in
                make.top.equalToSuperview()
                make.bottom.equalToSuperview()
                make.centerX.equalToSuperview()
            }
    
            headerView.snp.makeConstraints { make in
                make.width.equalTo(tableView)
            }
    
            headerImage.snp.makeConstraints{ (make) in
                make.width.equalTo(tableView).dividedBy(2)
                make.height.equalTo(headerImage.snp.width)
            }
        }
    }