1

I' struggling with some basic UIStackView distribution and alignment stuff.

I have an UICollectionViewCell which has a horizontal UIStackView at the contentView subview. This UIStackView has a vertical UIStackView for the three labels itself, and of course the UIImageView.

This is the code snippet for the screenshot below:

    func createSubViews() {

    // contains the UIStackview with the 3 labels and the UIImageView
    containerStackView = UIStackView()
    containerStackView.axis = .horizontal
    containerStackView.distribution = .fill
    containerStackView.alignment = .top
    contentView.addSubview(containerStackView)

    // the UIStackView for the labels
    verticalStackView = UIStackView()
    verticalStackView.axis = .vertical
    verticalStackView.distribution = .fill
    verticalStackView.spacing = 10.0
    containerStackView.addArrangedSubview(verticalStackView)

    categoryLabel = UILabel()
    categoryLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
    categoryLabel.textColor = UIColor.lightGray
    verticalStackView.addArrangedSubview(categoryLabel)

    titleLabel = UILabel()
    titleLabel.numberOfLines = 3
    titleLabel.lineBreakMode = .byWordWrapping
    verticalStackView.addArrangedSubview(titleLabel)

    timeLabel = UILabel()
    timeLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
    timeLabel.textColor = UIColor.lightGray
    verticalStackView.addArrangedSubview(timeLabel)

    // UIImageView
    imageView = UIImageView()
    imageView.contentMode = .scaleAspectFill
    imageView.clipsToBounds = true
    imageView.layer.cornerRadius = 5
    layer.masksToBounds = true
    containerStackView.addArrangedSubview(imageView)
}

enter image description here

What I want to achive is, that the "time label" ("3 days ago") is always placed at the bottom of each UICollectionViewCell (aligned with the bottom of the UIImageView), regardless of the different title label lines.

I've played with various UIStackView distributions, constraining the "time label" and with the hugging priority of the "title label".

But anyhow I can't get it right. Any hints?

wj313
  • 85
  • 3
  • 7
  • 1
    This does not sound like a good use case for UIStackView. I would suggest eliminating the stack views entirely. At that point, configuring the layout you want becomes trivial. – matt Mar 21 '18 at 18:25
  • Also, it's hard to know what you are doing about the constraints of the stack view itself. I don't see any sign that you are making the vertical stack view the height of the cell. So why would the bottom label be at the bottom of the cell? – matt Mar 21 '18 at 18:29

2 Answers2

1

UPDATE

Since you're setting titleLabel.numberOfLines = 3, one way to do this is simply to append three newlines to the title text. That will force titleLabel to always consume its full height of three lines, forcing timeLabel to the bottom.

That is, when you set titleLabel.text, do it like this:

titleLabel.text = theTitle + "\n\n\n"

ORIGINAL

If you let one of the labels stretch vertically, the stretched label's text will be centered vertically within the stretched label's bounds, which is not what you want. So we can't let the labels stretch vertically. Therefore we need to introduce a padding view that can stretch but is otherwise invisible.

If the padding view gets squeezed down to zero height, the stack view will still put spacing before and after it, leading to double-spacing between titleLabel and timeLabel, which you also don't want.

So we'll need to implement all the spacing using padding views. Change verticalStackView.spacing to 0.

Add a generic UIView named padding1 to verticalStackView after categoryLabel, before titleLabel. Constrain its height to equal 10.

Add a generic UIView named padding2 to verticalStackView after titleLabel, before timeLabel. Constrain its height to greater than or equal to 10 so that it can stretch.

Set the vertical hugging priorities of categoryLabel, titleLabel, and timeLabel to required, so that they will not stretch vertically.

Constrain the height of verticalStackView to the height of containerStackView so that it will stretch one or more of its arranged subviews if needed to fill the vertical space available. The only arranged subview that can stretch is padding2, so it will stretch, keeping the title text near the top and the time text at the bottom.

Also, constrain your containerStackView to the bounds of contentView and set containerStackView.translatesAutoresizingMaskIntoConstraints = false.

Result:

short title

long title

Here's my playground:

import UIKit
import PlaygroundSupport

class MyCell: UICollectionViewCell {

    var containerStackView: UIStackView!
    var verticalStackView: UIStackView!
    var categoryLabel: UILabel!
    var titleLabel: UILabel!
    var timeLabel: UILabel!
    var imageView: UIImageView!

    func createSubViews() {

        // contains the UIStackview with the 3 labels and the UIImageView
        containerStackView = UIStackView()
        containerStackView.axis = .horizontal
        containerStackView.distribution = .fill
        containerStackView.alignment = .top
        contentView.addSubview(containerStackView)

        // the UIStackView for the labels
        verticalStackView = UIStackView()
        verticalStackView.axis = .vertical
        verticalStackView.distribution = .fill
        verticalStackView.spacing = 0
        containerStackView.addArrangedSubview(verticalStackView)

        categoryLabel = UILabel()
        categoryLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
        categoryLabel.textColor = UIColor.lightGray
        verticalStackView.addArrangedSubview(categoryLabel)

        let padding1 = UIView()
        verticalStackView.addArrangedSubview(padding1)

        titleLabel = UILabel()
        titleLabel.numberOfLines = 3
        titleLabel.lineBreakMode = .byWordWrapping
        verticalStackView.addArrangedSubview(titleLabel)

        let padding2 = UIView()
        verticalStackView.addArrangedSubview(padding2)

        timeLabel = UILabel()
        timeLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
        timeLabel.textColor = UIColor.lightGray
        verticalStackView.addArrangedSubview(timeLabel)

        // UIImageView
        imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.layer.cornerRadius = 5
        layer.masksToBounds = true
        containerStackView.addArrangedSubview(imageView)

        categoryLabel.setContentHuggingPriority(.required, for: .vertical)
        titleLabel.setContentHuggingPriority(.required, for: .vertical)
        timeLabel.setContentHuggingPriority(.required, for: .vertical)
        imageView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
        containerStackView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            contentView.leadingAnchor.constraint(equalTo: containerStackView.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor),
            contentView.topAnchor.constraint(equalTo: containerStackView.topAnchor),
            contentView.bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor),
            verticalStackView.heightAnchor.constraint(equalTo: containerStackView.heightAnchor),
            padding1.heightAnchor.constraint(equalToConstant: 10),
            padding2.heightAnchor.constraint(greaterThanOrEqualToConstant: 10),
            ])
    }
}

let cell = MyCell(frame: CGRect(x: 0, y: 0, width: 320, height: 110))
cell.backgroundColor = .white
cell.createSubViews()
cell.categoryLabel.text = "MY CUSTOM LABEL"
cell.titleLabel.text = "This is my title"
cell.timeLabel.text = "3 days ago"
cell.imageView.image = UIGraphicsImageRenderer(size: CGSize(width: 110, height:110)).image { (context) in
    UIColor.blue.set()
    UIRectFill(.infinite)
}

PlaygroundPage.current.liveView = cell
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • I'm accepting this as answer, because for the use of an UIStackView and the explanation ... Tnx. – wj313 Mar 26 '18 at 15:29
0

The problem is the vertical stack view. You apparently want to say: the middle label's top should hug the MyCustomLabel bottom, but the 3 Days Ago bottom should hug the overall bottom. That is not something you can say to a stack view.

And even if that is not what you want to say, you would still need to make the vertical stack view take on the full height of the cell, and how are you going to do that? In the code you showed, you don't do that at all; in fact, your stack view has zero size based on that code, which will lead to all sorts of issues.

I would suggest, therefore, that you just get rid of all the stack views and just configure the layout directly. Your layout is an easy one to configure using autolayout constraints.

matt
  • 515,959
  • 87
  • 875
  • 1,141