Search code examples
swiftuikituistackview

UIStackView and a placeholder view in another UIStackView problem


There is a problem if you have a UIStackView(testStack) and a placeholder UIView(testView) inside another UIStackView(mainStack). It is meant that if there is no content in the testStack it will collapse, and the testView will take all the space. There is even a content hugging priority set to maximum for the testStack so it should collapse its height to 0 when there are no subviews. But it does not. How to make it collapse when there is no content?

PS If there are items in the testStack, everything works as expected: testView takes all available space, testStack takes only the space to fit its subviews.

class AView: UIView {
    lazy var mainStack: UIStackView = {
        let stack = UIStackView()
        stack.axis = .vertical
        stack.backgroundColor = .gray
        stack.addArrangedSubview(self.testStack)
        stack.addArrangedSubview(self.testView)
        return stack
    }()
    
    let testStack: UIStackView = {
        let stack = UIStackView()
        stack.backgroundColor = .blue
        stack.setContentHuggingPriority(.init(1000), for: .vertical)
        return stack
    }()
    
    let testView: UIView = {
        let view = UIView()
        view.backgroundColor = .red
        return view
    }()
    
    init() {
        super.init(frame: .zero)
        backgroundColor = .yellow
        addSubview(mainStack)
        mainStack.translatesAutoresizingMaskIntoConstraints = false
        mainStack.topAnchor.constraint(equalTo: topAnchor).isActive = true
        mainStack.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        mainStack.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        mainStack.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Solution

  • When auto-layout arranges subviews in a UIStackView, it looks at:

    • the stack view's .distribution property
    • the subviews' height constraints (if given)
    • the subviews' Intrinsic Content Size

    Since you have not specified a .distribution property, mainStack is using the default of .fill.

    A UIStackView has NO Intrinsic Content Size, so auto-layout says "testStack has a height of Zero"

    A UIView has NO Intrinsic Content Size, so auto-layout says "testView has a height of Zero"

    Since the distribution is fill, auto-layout effectively says: "the heights of the arranged subviews are ambiguous, so let's give the last subview a height of Zero, and fill mainStack with the first subview.

    Setting .setContentHuggingPriority will have no effect, because there is no intrinsic height to "hug."

    If you set mainStack's .distribution = .fillEqually, you will get (as expected) testStack filling the top half, and testView filling the bottom half.

    If you set mainStack's .distribution = .fillProportionally, you will get the same result... testStack filling the top half, and testView filling the bottom half, because .fillProportionally uses the arranged subviews' Intrinsic Content Sizes... in this case, they are both Zero, so "proportional" will be equal.

    If you set mainStack's .distribution = .equalSpacing or .distribution = .equalCentering, you won't see either testStack or testView ... auto-layout will give each of them a height of Zero, and fill the rest of mainStack with (empty) "spacing."

    If your goal is to have testStack "disappear" if it is empty, you can either:

    • set it hidden, or
    • subclass it and give it an intrinsic height