Wrap Items in a Horizontal Uistackview on Multiple Lines

Multiline label in UIStackView

The correct answer is here:

https://stackoverflow.com/a/43110590/566360


  1. Embed the UILabel inside a UIView (Editor -> Embed In -> View)
  2. Use constraints to fit the UILabel to the UIView (for example, trailing space, top space, and leading space to superview constraints)

The UIStackView will stretch out the UIView to fit properly, and the UIView will constrain the UILabel to multiple lines.

Horizontal UIStackView with two label (one multiline label, one one-line)

To try and simplify...

Forget calculating any widths... what matters is the horizontal Content Hugging and Content Compression Resistance

Leave the left (blue) label at the defaults:

Content Hugging Priority
Horizontal: 251

Content Compression Resistance Priority:
Horizontal: 750

But set the right (orange) label to:

Content Hugging Priority
Horizontal: 1000

Content Compression Resistance Priority:
Horizontal: 1000

Results:

Sample Image

The only other issue would be if the text in the right-side label exceeds the full width of the view -- but you haven't indicated that you might need that much text.

StackView horizontally wrap content

Have you tried changing the alignment of stackViewAir and stackViewSleep to .center? I think that will do the trick here. Thanks

How to make horizontal UIStackView wrap height content with UIImageView as its children?

You cannot get this result with only Storyboard setup -- you will need some code.

Based on your image showing your desired output, you want each "row" to:

  • have 3 images
  • show them at original aspect-ratio
  • small spacing between images (such as 4-pts)
  • fit so the widths and heights maintain the ratios

First comment - forget Distribution = Fill Proportionally on the stack views.

For this layout, the stack view should be:

  • Axis: Horizontal
  • Alignment: Fill
  • Distribution: Fill
  • Spacing: 4

So, what we need to do is constrain each imageView's aspect ratio based on its image's aspect ratio (height / width).

Here is a full example... it uses these 6 images:

Sample Image Sample Image Sample Image Sample Image Sample Image Sample Image

We'll also embed the horizontal stack view(s) in a vertical stack view, using the same settings (expect Axis: Vertical).

class AdjustStackViewController: UIViewController {

var vStack = UIStackView()

var picNames: [String] = []

override func viewDidLoad() {
super.viewDidLoad()

// six images, named "p1" - "p6"
for i in 1...6 {
picNames.append("p\(i)")
}

// vertical stack view
vStack.axis = .vertical
vStack.spacing = 4

// use auto-layout
vStack.translatesAutoresizingMaskIntoConstraints = false

// add it to the view
view.addSubview(vStack)

// respect safe-area
let g = view.safeAreaLayoutGuide

NSLayoutConstraint.activate([
// constrain vStack
// Top + 20
// Leading and Trailing 0
vStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
vStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
vStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
])

fillStacks()
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// re-fill images in new (shuffled) order
fillStacks()
}

func fillStacks() -> Void {

// remove existing horizontal stack views (if needed)
vStack.arrangedSubviews.forEach { v in
v.removeFromSuperview()
}

let shuffledImages = picNames.shuffled()

// two horizontal stack views each with 3 images
var picNum: Int = 0
for _ in 1...2 {
let hStack = UIStackView()
hStack.spacing = 4
vStack.addArrangedSubview(hStack)
for _ in 1...3 {
// make sure we can load the images
guard let img = UIImage(named: shuffledImages[picNum]) else {
fatalError("Could not load images!")
}
// create Image View
let imgView = UIImageView()
// set the image
imgView.image = img
// proportional constraint based on image dimensions
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: img.size.height / img.size.width).isActive = true
// add to hStack
hStack.addArrangedSubview(imgView)
// increment pic number
picNum += 1
}
}

}

}

Here's how it looks:

Sample Image

When you run this, each time you tap the view it will shuffle the order of the images, so you can see how the layout adapts.

iOS: UILabel multi-line text get truncated in horizontal UIStackView

alignment

/* The layout of the arrangedSubviews transverse to the axis;
e.g., leading/trailing edges in a vertical stack
*/
@property(nonatomic) UIStackViewAlignment alignment;

For .fill

StackView will automatically generate these layout attributes for you.

 imageView.bottom == label.bottom
imageView.top == label.top

the size of stackView depend on the 'voice' image's size or size layout attributes of imageView as it is fixed instead of adjustable UILable which namely have the low priority.


For .center

StackView achieve a decent height by look for the max height between your label and imageView.

 _UILayoutSpacer.bottom >= imageView.bottom
_UILayoutSpacer.bottom >= label.bottom
_UILayoutSpacer.top <= imageView.top
_UILayoutSpacer.top <= label.top

UIStackView distribution and alignment of a multiline UILabel

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

UIStackView with wrap content width

Step 1: Drag a StackView add left, right, top/bottom constraint to it. (ignore error for now)

Sample Image

Step 2: Drag two labels and add it to stackView (continue to ignore warning)

Sample Image

Step 3: Now you need to resolve horizontal content ambiguity so change the content hugging priority of left label to 250 while leaving the right labels content hugging priority at 251

Sample Image

Step 4: Now comes your question of warp content for vertical height :) Simply select the right label and set its content compression resistance to 999 while leaving left label's content compression resistance at 751

Sample Image

If you are wondering whats happening with content compression resistance and content hugging priority and all, please read apple docs on the same for more detail

Just to give a gist of what am doing, in iOS UILabels have implicit content size, so they take size based on content. So is Stack View. Now because StackView has to deduce its height based on two labels it has and both of them have same content compression resistance and end up having different height in order to resolve confusion it looks for further assistance from you. You do that specifying that right label resists more to change in its implicit vertical height compared to left label.

That means now stack view knows that it has to consider right labels height to adjust its height while it can override the implicit size of left label :)

Confirmation of logic:

Sample Image

On changing right label font stack view height automatically increased and increased the height of left label as well :)

EDIT on Comment by OP:

As OP suggested in comment that conern is width and not height, step 4 is not necessary, your problem is resolved at the end of step 3. On setting content hugging priority it self

on changing right labels content UI updates as shown below

Sample Image
Thats all :) Hope that helps



Related Topics



Leave a reply



Submit