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.

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)
    }
}

if you are searching for a more generic solution here is what works for me:
//
//  UIStackView+.swift
//
import UIKit
extension UIStackView {
    // Check if a given set of views and subviews fit into their desired frame
    private func checkSubviews(views: [UIView], includeSubviews: Bool = false) -> Bool {
        for view in views {
            let idealSize = view.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: view.bounds.height))
            if idealSize.width > view.bounds.width {
                axis = .vertical
                return false
            }
            if includeSubviews {
                if checkSubviews(views: view.subviews, includeSubviews: true) == false {
                    return false
                }
            }
        }
        return true
    }
    @discardableResult
    func updateAxisToBestFit(views: [UIView], includeSubviews: Bool = false) -> UILayoutConstraintAxis {
        self.axis = .horizontal
        setNeedsLayout()
        layoutIfNeeded()
        let subviewsFit = checkSubviews(views: views, includeSubviews: includeSubviews)
        guard subviewsFit == false else { return .horizontal }
        self.axis = .vertical
        setNeedsLayout()
        layoutIfNeeded()
        return .vertical
    }
    @discardableResult
    func updateAxisToBestFit() -> UILayoutConstraintAxis {
        return updateAxisToBestFit(views: self.arrangedSubviews, includeSubviews: true)
    }
}
The usage would be then:
class MyCellView: UITableCellView {
    @IBOutlet weak var myStackView: UIStackView!
    public func configure(with model: MyCellViewModel) {
        // update cell content according to model
        myStackView?.updateAxisToBestFit()
    }
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With