Frankenstein
Frankenstein

Reputation: 16361

Give more priority for the first UILabel in a UIStackView when frame is smaller than required

I have an array of labels: [UILabel], the count is according to the number of elements in an array. I'm adding them into a UIStackView by looping over them. At a point, the number of UILabels exceeds the frame of the UIStackView in this situation I want the UILabels that are added first to be shown and those added later to be hidden. What happens is the exact opposite. Like this:-

screenshot

I would like text 1 preferred to be shown over text 2 and so on. Here's the code:

override func viewDidLoad() {
    super.viewDidLoad()

    let stackView = UIStackView()
    stackView.alignment = .top
    stackView.axis = .vertical

    view.addSubview(stackView)
    stackView.center = view.center
    stackView.frame.size = CGSize(width: 100, height: 24)

    (0..<4).forEach {
        let label = UILabel()
        label.text = "text \($0)"
        label.backgroundColor = .red
        stackView.addArrangedSubview(label)
    }
}

FYI: I do not want to reverse the order.

Update: Thanks to @DonMag and @Rob I've set content compression resistance priority in my code. Unfortunately, I'm getting a different result for some reason from what @DonMag has depicted in his screenshots. Here's my updated code:

override func viewDidLoad() {
    super.viewDidLoad()

    let stackView = UIStackView()
    stackView.alignment = .leading
    stackView.axis = .vertical

    view.addSubview(stackView)
    stackView.frame = CGRect(x: 100, y: 100, width: 100, height: 150)

    (0..<15).forEach {
        let label = UILabel()
        label.text = "text \($0)"
        label.backgroundColor = .red
        stackView.addArrangedSubview(label)
    }
    var p: Float = 1000
    for v in stackView.arrangedSubviews {
        v.setContentCompressionResistancePriority(UILayoutPriority(rawValue: p), for: .vertical)
        p -= 1
    }
}

And here's the result that I'm talking about:

screenshot 2

Upvotes: 0

Views: 2334

Answers (1)

DonMag
DonMag

Reputation: 77690

This should do it:

    var p: Float = 1000
    for v in stackView.arrangedSubviews {
        v.setContentCompressionResistancePriority(UILayoutPriority(rawValue: p), for: .vertical)
        p -= 1
    }
    

A bit of an odd way to use a stack view though, and it may not give you what you want after all.

For example, with a 100 x 150 frame, and 14 labels, we get:

enter image description here

as we see, the bottom label is a little "off."

And, with only 5 labels, we get:

enter image description here

The top label height gets stretched, unless we do the same thing with Content Hugging priority... in which case the bottom label would get stretched.

A better route would be to embed the stack view in a UIView, constrain Top / Leading / Trailing, set the height of the "container" UIView, and then make sure to set .clipsToBounds = true on the container view.

With 14 labels:

enter image description here

With 5 labels:

enter image description here

Here's the code to play with...

Method 1:

class FrankMethod1ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let stackView = UIStackView()
        stackView.alignment = .leading
        stackView.axis = .vertical
        
        view.addSubview(stackView)
        stackView.frame = CGRect(x: 100, y: 100, width: 100, height: 150)
        
        // change to (0..<5) to see the stretching
        (0..<15).forEach {
            let label = UILabel()
            label.text = "text \($0)"
            label.backgroundColor = .red
            stackView.addArrangedSubview(label)
        }

        var p: Float = 1000
        for v in stackView.arrangedSubviews {
            v.setContentCompressionResistancePriority(UILayoutPriority(rawValue: p), for: .vertical)
            p -= 1
        }

    }

}

Method 2:

class FrankMethod2ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let cView = UIView()
        cView.frame = CGRect(x: 100, y: 100, width: 100, height: 150)
        cView.clipsToBounds = true
        
        // so we can see the view frame
        cView.backgroundColor = .cyan
        
        view.addSubview(cView)
        
        let stackView = UIStackView()
        stackView.alignment = .leading
        stackView.axis = .vertical
        
        cView.addSubview(stackView)
        
        stackView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: cView.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: cView.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: cView.trailingAnchor),
        ])
        
        // change to (0..<5) to see NO stretching
        (0..<15).forEach {
            let label = UILabel()
            label.text = "text \($0)"
            label.backgroundColor = .red
            stackView.addArrangedSubview(label)
        }
        
    }
    
}

Edit

I don't think it's uncommon to see differences between Playground execution and Sim / Device.

Here's what I get:

enter image description here

when running this code in a Playground ... top two are "Method 1" with 15 & 5 labels, bottom two are "Method 2" with 15 & 5 labels:

import UIKit
import PlaygroundSupport

class PlaygroundFrankViewController: UIViewController {

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .white
        self.view = view
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let stackView1 = UIStackView()
        stackView1.alignment = .leading
        stackView1.axis = .vertical
        
        view.addSubview(stackView1)
        stackView1.frame = CGRect(x: 50, y: 50, width: 100, height: 150)
        
        // change to (0..<5) to see the stretching
        (0..<15).forEach {
            let label = UILabel()
            label.text = "text \($0)"
            label.backgroundColor = .red
            stackView1.addArrangedSubview(label)
        }
        
        var p: Float = 1000
        for v in stackView1.arrangedSubviews {
            v.setContentCompressionResistancePriority(UILayoutPriority(rawValue: p), for: .vertical)
            p -= 1
        }

        let stackView2 = UIStackView()
        stackView2.alignment = .leading
        stackView2.axis = .vertical
        
        view.addSubview(stackView2)
        stackView2.frame = CGRect(x: 200, y: 50, width: 100, height: 150)
        
        // change to (0..<5) to see the stretching
        (0..<5).forEach {
            let label = UILabel()
            label.text = "text \($0)"
            label.backgroundColor = .red
            stackView2.addArrangedSubview(label)
        }
        
        p = 1000
        for v in stackView2.arrangedSubviews {
            v.setContentCompressionResistancePriority(UILayoutPriority(rawValue: p), for: .vertical)
            p -= 1
        }
        
        
        let cView1 = UIView()
        cView1.frame = CGRect(x: 50, y: 220, width: 100, height: 150)
        cView1.clipsToBounds = true
        
        // so we can see the view frame
        cView1.backgroundColor = .cyan
        
        view.addSubview(cView1)
        
        let stackView3 = UIStackView()
        stackView3.alignment = .leading
        stackView3.axis = .vertical
        
        cView1.addSubview(stackView3)
        
        stackView3.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            stackView3.topAnchor.constraint(equalTo: cView1.topAnchor),
            stackView3.leadingAnchor.constraint(equalTo: cView1.leadingAnchor),
            stackView3.trailingAnchor.constraint(equalTo: cView1.trailingAnchor),
        ])
        
        // change to (0..<5) to see NO stretching
        (0..<15).forEach {
            let label = UILabel()
            label.text = "text \($0)"
            label.backgroundColor = .red
            stackView3.addArrangedSubview(label)
        }
        
        let cView2 = UIView()
        cView2.frame = CGRect(x: 200, y: 220, width: 100, height: 150)
        cView2.clipsToBounds = true
        
        // so we can see the view frame
        cView2.backgroundColor = .cyan
        
        view.addSubview(cView2)
        
        let stackView4 = UIStackView()
        stackView4.alignment = .leading
        stackView4.axis = .vertical
        
        cView2.addSubview(stackView4)
        
        stackView4.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            stackView4.topAnchor.constraint(equalTo: cView2.topAnchor),
            stackView4.leadingAnchor.constraint(equalTo: cView2.leadingAnchor),
            stackView4.trailingAnchor.constraint(equalTo: cView2.trailingAnchor),
        ])
        
        // change to (0..<5) to see NO stretching
        (0..<5).forEach {
            let label = UILabel()
            label.text = "text \($0)"
            label.backgroundColor = .red
            stackView4.addArrangedSubview(label)
        }

    }
    
}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = PlaygroundFrankViewController()

Upvotes: 1

Related Questions