UIStackView, 2 labels: Change Axis based on size
Asked Answered
I

2

6

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

Interested answered 2/5, 2018 at 22:9 Comment(3)
Have you created stackview programmatically or in storyboard?Demarcusdemaria
I've created the UIStackView programmatically. Still, it shouldn't matter for the scope of this question.Interested
If you want to know when the content size changed, this might help you: #18951832Gratulant
V
7

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

Varese answered 3/5, 2018 at 15:57 Comment(3)
It's definitely the most creative answer to this question. Both of these StackViews could be encapsulated into a single class with the aforementioned behavior and voilà! Thank you!Interested
@RichardTopchiy when answering questions I like to not put it in a subclass so that others can understand it. Glad it helped.Varese
Clear, but in my app there are plenty of views like that, so the subclass should work for me.Interested
A
5

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

Asiatic answered 17/9, 2019 at 12:12 Comment(2)
Does it work with UITableViewHeaderFooterView? The problem that I am facing it doesn't increase the height of the section headerIntenerate
my solution is not setting they height of a cell but changing the axis of a stackview (so instead of putting cells in x-axis it will change it to a y-axis. The height should be set to auto.Asiatic

© 2022 - 2024 — McMap. All rights reserved.