iosautolayoutuilabeluistackviewintrinsic-content-size

UIStackView, 2 labels: Change Axis based on size


I have two labels embedded inside a horizontal UIStackView. One of the labels might grow too large in size, therefore, one of them is truncated.

Or, their sizes are being split in some proportion and they grow vertically.

What I'm trying to achieve is to make UIStackView change axis to vertical if the labels do not fit.

Please, refer to the two figures attached below:

Figure 1: Labels fit in one line, a horizontal axis is used.
Figure 2: One of the labels is multiline. A vertical axis is used.

The behavior is similar when using UICollectionViewFlowLayout.

Example StackView behavior


Solution

  • Give this a go. You just have to measure the text and you have to have a stack view wrapped with another stackview.

    import UIKit
    
    class ViewController: UIViewController {
    
        var stackView : UIStackView?
        var outerStackView : UIStackView?
        var label1 = UILabel()
        var label2 = UILabel()
    
        var heightAnchor : NSLayoutConstraint?
        var widthAnchor : NSLayoutConstraint?
        var button : UIButton?
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
            createStackView()
            createLabelsAndAddToStack()
    
            button = UIButton(frame: CGRect(x: 20, y: self.view.frame.height - 60, width: self.view.frame.width - 40, height: 40))
            button?.backgroundColor = .green
            button?.setTitleColor(.black, for: .normal)
            button?.setTitle("Toggle Text", for: .normal)
            button?.addTarget(self, action: #selector(toggleText), for: .touchUpInside)
            button?.setTitleColor(.gray, for: .highlighted)
            self.view.addSubview(button!)
        }
    
        func createStackView(){
    
            outerStackView = UIStackView(frame: CGRect(x: 20, y: 20, width: self.view.bounds.width - 40, height: 100))
            outerStackView?.axis = .vertical
            outerStackView?.distribution = .fill
            outerStackView?.alignment = .center
            outerStackView?.translatesAutoresizingMaskIntoConstraints = false
    
            self.view.addSubview(outerStackView!)
    
            outerStackView?.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 40).isActive = true
            outerStackView?.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -40).isActive = true
            outerStackView?.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 50).isActive = true
    
    
            stackView = UIStackView(frame: CGRect(x: 20, y: 20, width: self.view.bounds.width - 40, height: 100))
            stackView?.alignment = .center
            stackView?.spacing = 20
            stackView?.distribution = .fillEqually
            stackView?.axis = .horizontal
            outerStackView?.addArrangedSubview(stackView!)
    
            heightAnchor = stackView?.heightAnchor.constraint(equalTo: outerStackView!.heightAnchor)
            widthAnchor = stackView?.widthAnchor.constraint(equalTo: outerStackView!.widthAnchor)
    
        }
    
        func createLabelsAndAddToStack(){
            label1 = UILabel(frame: .zero)
            label1.textColor = .black
            label1.font = UIFont.systemFont(ofSize: 17)
            label1.text = "Label 1"
            label1.numberOfLines = 0
            stackView?.addArrangedSubview(label1)
    
            label2 = UILabel(frame: .zero)
            label2.font = UIFont.systemFont(ofSize: 17)
            label2.textColor = .green
            label2.text = "Label 2"
            label2.numberOfLines = 0
            stackView?.addArrangedSubview(label2)
    
        }
    
        func adaptStackView(){
    
            self.outerStackView?.layoutIfNeeded()
            self.stackView?.layoutIfNeeded()
            //let maxWidth = label2.frame.width
            //if you want it to be the max it could be
            let maxWidth = (self.outerStackView!.frame.width - stackView!.spacing)/2
            let height = label2.font.lineHeight
    
            if let label1Text = label1.text{
                //lets measure
                if label1Text.width(withConstraintedHeight: height, font: label1.font) > maxWidth{
                    outerStackView?.axis = .horizontal
                    outerStackView?.distribution = .fill
    
                    stackView?.axis = .vertical
                    stackView?.distribution = .fill
                    stackView?.alignment = .leading
    
                    widthAnchor?.isActive = true
                    heightAnchor?.isActive = true
    
                }else{
                    outerStackView?.axis = .vertical
                    outerStackView?.distribution = .fill
    
                    stackView?.alignment = .center
                    stackView?.axis = .horizontal
                    stackView?.distribution = .fillEqually
    
                    stackView?.removeConstraints(self.stackView!.constraints)
                    widthAnchor?.isActive = false
                    widthAnchor?.isActive = false
                }
    
                UIView.animate(withDuration: 0.5) {
                    self.view.layoutIfNeeded()
                }
            }
        }
    
        @objc func toggleText(){
            if label1.text == "Label 1"{
                label1.text = "This is some longer text to test everything out to see how we are doing"
    
            }else{
                label1.text = "Label 1"
            }
            adaptStackView()
        }
    
    }
    
    extension String{
        func width(withConstraintedHeight height: CGFloat, font: UIFont) -> CGFloat {
            let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
            let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin], attributes: [.font: font], context: nil)
    
            return ceil(boundingBox.width)
        }
    }
    

    stackgif