iosswiftuikit

UIStackView uses the width of the smallest arranged view instead of largest


Here is how I understand how UIStackView works from Apple's document:

  1. If I do not specify width or add related constraint, uikit will use auto layout to figure it out for me. And it will use the largest subview width. As stated from apple doc: https://developer.apple.com/documentation/uikit/uistackview

Quote:

Perpendicular to the stack view’s axis, its fitting size is equal to the size of the largest arranged view. 2. If I use alignment = .fill, all subview will be resized to the same size (stackview's size)

So I thought, if I created a simple vertical stack view containing 3 subview with different width, and I don't specify the stackview's width. UIKit will first do 1 then 2 above and resize everything using the largest subview, right?

I was wrong, the smallest subview is used. I don't understand why.

Here is code and the screenshot of the result.

result

Here is my code (I wrap it in swiftUI wrapper so you can see the canvas display directly):

import Foundation
import UIKit
import SwiftUI
import SnapKit

class TestViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        // Create the stack view with a vertical axis
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.backgroundColor = .gray
        stackView.spacing = 20
        stackView.alignment = .fill  // Change this to .leading to see the effect
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        // Add the stack view to the view controller's view
        view.addSubview(stackView)
        
        // Constraints for the stack view
        stackView.snp.makeConstraints { make in
            make.top.equalTo(view)
            make.left.equalTo(view)
        }
        
        // Create three views with different widths
        let view1 = UIView()
        view1.translatesAutoresizingMaskIntoConstraints = false
        view1.backgroundColor = .red
        view1.snp.makeConstraints { make in
            make.width.equalTo(100)
            make.height.equalTo(50)
        }
        
        let view2 = UIView()
        view2.translatesAutoresizingMaskIntoConstraints = false
        view2.backgroundColor = .green
        view2.snp.makeConstraints { make in
            make.width.equalTo(200)
            make.height.equalTo(50)
        }
        
        let view3 = UIView()
        view3.translatesAutoresizingMaskIntoConstraints = false
        view3.backgroundColor = .blue
        view3.snp.makeConstraints { make in
            make.width.equalTo(300)
            make.height.equalTo(50)
        }
        
        // Add views to the stack view
        stackView.addArrangedSubview(view2)
        stackView.addArrangedSubview(view1)
        stackView.addArrangedSubview(view3)
    }
    
}



struct TestViewControllerTestView : UIViewControllerRepresentable {
    
    typealias UIViewControllerType = TestViewController
    
    func makeUIViewController(context: Context) -> TestViewController {
        return TestViewController()
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        
    }
}


struct TestViewController_Preview : PreviewProvider {
    static var previews: some View {
        TestViewControllerTestView()
    }
}


Solution

  • UIStackView can take a little experimentation to fully understand how it lays out views.

    In general, we use constraints to set the size of the stack view and then allow it to control the subviews layout.

    In the case of your vertical axis stack view, with no width or height constraint...

        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 20
        stackView.alignment = .fill
    

    If we do not explicitly set width of any arranged subviews, auto-layout will use the view with the greatest .intrinsicContentSize.width to set the stack view width, and then fill the other views.

    If we change your code to this:

        let v1 = UILabel()
        v1.text = "Short text"
        v1.backgroundColor = UIColor(red: 1.0, green: 0.6, blue: 0.6, alpha: 1.0)
        v1.snp.makeConstraints { make in
            make.height.equalTo(50)
        }
        
        let v2 = UILabel()
        v2.text = "Medium length text"
        v2.backgroundColor = .green
        v2.snp.makeConstraints { make in
            make.height.equalTo(50)
        }
        
        let v3 = UILabel()
        v3.text = "Longest amount of text in the label"
        v3.backgroundColor = .cyan
        v3.snp.makeConstraints { make in
            make.height.equalTo(50)
        }
        
        stackView.addArrangedSubview(v2)
        stackView.addArrangedSubview(v3)
        stackView.addArrangedSubview(v1)
    

    we get this output:

    enter image description here

    If we set the width of v1 (the shortest text) to, let's say, 120:

        v1.snp.makeConstraints { make in
            make.width.equalTo(120)
            make.height.equalTo(50)
        }
    

    we end up with this:

    enter image description here

    If we set it to 320:

        v1.snp.makeConstraints { make in
            make.width.equalTo(320)
            make.height.equalTo(50)
        }
    

    we get this:

    enter image description here

    In both cases, we're explicitly setting the width of one of the subviews, but NOT setting a width on either of the other two.

    As I mentioned in my comment, your code is saying:

    "Hey auto-layout... make the 3 views different widths but also make them the same widths."

    Of course, auto-layout cannot do that. You should see plenty of error messages in the debug console. In this specific case, auto-layout is using the narrowest width constraint -- but, in all error cases, the actual behavior is undefined.

    If you want all 3 views to stretch the width of the stack view, you need to either:

    A - only set the width constraint on the widest view, or

    B - constrain the width of the stack view, but NOT any of the arranged subviews.

    Which approach to take will depend on various factors which are not known here, as it's unlikely that red, green, blue "bars" is your actual goal.