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