Search code examples
swiftuistackviewclean-architecture

How to make Vertical StackView subviews (same specific heights) start from TOP, Swift?


I'm creating a vertical UIStackView where arranged subviews will start from top. Subviews quantity will be 5 at most. Here are my expectation, reality and code. Any Idea?

Expectation

enter image description here

Current situation

enter image description here

Code

var homeVStack: UIStackView = {
    let stackView = UIStackView()
    stackView.axis = .vertical
    stackView.alignment = .top
    stackView.distribution = .equalSpacing
    stackView.spacing = 20
    stackView.translatesAutoresizingMaskIntoConstraints = false
    return stackView
}()

private func loadData() {
    if let homeFormList = data?.homeTeamForm {
        for homeForm in homeFormList {
            let teamFormView = SimpleScoreView()
            teamFormView.teamForm = homeForm
            teamFormView.heightAnchor.constraint(equalToConstant: 50).isActive = true
            teamFormView.backgroundColor = .yellow
            homeVStack.addArrangedSubview(teamFormView)
        }
    }
}

Solution

  • You are explicitly setting the Height of the subviews:

    teamFormView.heightAnchor.constraint(equalToConstant: 50).isActive = true
    

    So, do NOT give your stackView a Bottom anchor or Height constraint.

    Here's a quick example...

    SimpleScoreView -- view with a centered label:

    class SimpleScoreView: UIView {
        let scoreLabel = UILabel()
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            self.backgroundColor = .yellow
            scoreLabel.textAlignment = .center
            scoreLabel.translatesAutoresizingMaskIntoConstraints = false
            scoreLabel.backgroundColor = .green
            addSubview(scoreLabel)
            NSLayoutConstraint.activate([
                scoreLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
                scoreLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
            ])
        }
    }
    

    Basic View Controller -- each tap will add another "Score View":

    class ViewController: UIViewController {
        
        var homeVStack: UIStackView = {
            let stackView = UIStackView()
            stackView.axis = .vertical
            // these two are not needed
            //stackView.alignment = .top
            //stackView.distribution = .equalSpacing
            stackView.spacing = 20
            stackView.translatesAutoresizingMaskIntoConstraints = false
            return stackView
        }()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.addSubview(homeVStack)
            
            // respect safe area
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                // constrain stack view Top / Leading / Trailing
                homeVStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
                homeVStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
                homeVStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
            ])
            
            loadData()
        }
    
        // for this example, loadData() will add one new
        //  SimpleScoreView each time it's called
        private func loadData() {
            let v = SimpleScoreView()
            v.heightAnchor.constraint(equalToConstant: 50).isActive = true
            v.scoreLabel.text = "\(homeVStack.arrangedSubviews.count + 1)"
            homeVStack.addArrangedSubview(v)
        }
    
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            loadData()
        }
        
    }
    

    Results:

    enter image description here

    enter image description here

    enter image description here