Uistackview Distribution Fill Equally

UIStackView Distribution Fill Equally

As a confirmation, this is the current behavior:

Sample Image

And this is what are you asking for is:

Sample Image

In order to achieve it, you could follow this simple trick:

P.S: I assume that you added the needed appropriate constraints for your stack view.

If your stack view doesn't have a "height" constraint, add one:

Sample Image

Now, add it as an IBOutlet to the assigned ViewController; In my example, I'm calling it stackHeight:

@IBOutlet weak var stackHeight: NSLayoutConstraint!

On the event that you want to hide the view (in my example, I'm hiding the orange button based on IBAction assigned to itself, when tapping on it, should be hidden), you need to get the height of the view that you want to hide and subtract from stackHeight.constant:

    @IBAction func orangeTapped(_ sender: AnyObject) {
orange.isHidden = true

// here we go:
stackHeight.constant = stackHeight.constant - orange.frame.size.height
}

UIStackView Distribution Fill Equally when inner view is hidden

Your problem is that you're always adding some value to your trailing constant (cell.view2.frame.size.width) and never subtract it again (in the code you posted). Table view cells are being reused / recycled. So when you scroll and an old table view cell that scrolled out of the view and had only 2 arranged subviews is being resued but this time with 3 arranged subviews, it still has this extra constant you set for your stackViewTrailing constraint.

Try setting an absolute value for your constraint's constant rather that adding a value to it using the += operator, i.e. instead of

cell.stackViewTrailing.constant += cell.view2.frame.size.width

do this:

cell.stackViewTrailing.constant = cell.view2.frame.size.width + padding

That should solve the problem. (That is, if I understand your problem correctly.)

Note:

You might reconsider if a stack view is the right choice here. Stack views always adjust their arranged subviews' widths when one of them is hidden (or revealed) so that the remaining visible one take up all the available space. If you want to have 3 fixed columns no matter how many of them are visible, it might be a better idea to just use plain constraints and give all three subviews equal width constraints.

StackViews in StackViews Fill All Equally Based On Smallest Item

This is a little tricky...

We need to allow the "dot rows" (horizontal stack views) to center themselves, rather than stretching the width of the main stack view.

We also need to actually calculate the dot widths instead of using .fillEqually so we don't end up with parital-point sizes.

For example:

  • main stack width is 75
  • with 1-pt spacing
  • 3 horizontal dots

Available width is stack view width minus number of "spaces" x spacing:

75 - 2 = 73
73 / 3 = 24.333333...

On a @2x scale device, the actual widths of the 3 views will be:

24.5 : 24 : 24.5

Not very noticeable with just 3 "dots" but it becomes very noticeable when we get to a 1, 3, 5, 7, 9, 8, 7, 6, 5 pattern.

.fillEqually is on the left, calculated whole-number point sizes on the right:

Sample ImageSample Image

Here's some example code:

class DotsViewController: UIViewController {

let patterns: [[Int]] = [
[3, 3, 3],
[3],
[1, 2, 3],
[1, 3, 5, 4, 3],
[1, 3, 5, 7, 9, 8, 7, 6, 5],
]

let mainStack: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .fill
v.distribution = .equalSpacing
return v
}()

// space between dots
let dotSpacing: CGFloat = 1

override func viewDidLoad() {
super.viewDidLoad()

view.addSubview(mainStack)

let g = view.safeAreaLayoutGuide

NSLayoutConstraint.activate([
// constrain main stack view 20-pts from top / leading / bottom
mainStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),

// width: 75
mainStack.widthAnchor.constraint(equalToConstant: 75.0),
])

patterns.forEach { a in
// create a vertical stack view
// to hold the "rows" of horizontal stack views (containing the dots)
let vBlockStack = UIStackView()
vBlockStack.axis = .vertical
vBlockStack.alignment = .center
vBlockStack.distribution = .fill
vBlockStack.spacing = dotSpacing
// add it to the main stack view
mainStack.addArrangedSubview(vBlockStack)

// calculate dot size
// needs to be a whole number so we don't get
// half-point sizes
let maxDots:CGFloat = CGFloat(a.max()!)
let availableWidth:CGFloat = 75.0 - ((maxDots - 1) * dotSpacing)
let w:CGFloat = floor(availableWidth / maxDots)

a.forEach { numDots in
// create a horizontal stack view
let hDotStack = UIStackView()
hDotStack.axis = .horizontal
hDotStack.alignment = .fill
hDotStack.distribution = .fill
hDotStack.spacing = dotSpacing
// add it to the vertical block stack view
vBlockStack.addArrangedSubview(hDotStack)
for _ in 0..<numDots {
let v = UIImageView()
v.contentMode = .scaleAspectFit
v.image = UIImage(systemName: "circle.fill")
v.tintColor = .red
// add view to dot stack view
hDotStack.addArrangedSubview(v)
// set dots image view size constraints
v.widthAnchor.constraint(equalToConstant: w).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
}
}

}

}

}

That produces this output:

Sample Image

UIStackView distributing with equalCentering

This may help you understand...

Each "row" of three green labels is a Horizontal Stack View with Spacing: 8 and Distribution set to:

  • Fill
  • Fill Equally
  • Fill Proportionally
  • Equal Centering
  • Equal Spacing

Sample Image

As you can see, with Distribution: Equal Centering, the stack view arranges its subviews so their centers are equally spaced.

What you probably want is equal spacing on the sides and in-between:

Sample Image

To get that layout, use Distribution: Fill and add an empty "spacer" view in the stack, so you have:

spacer1 - viewCountLesson - spacer2 - viewCountStudent - spacer3

then set spacer2 width equal to spacer1 and spacer3 width equal to spacer1.

Here is the code used to create that:

class NewStackViewController: UIViewController {

let studentCount: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .yellow
v.text = "2"
return v
}()

let lessonCount: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .yellow
v.text = "1"
return v
}()

let labelStudent: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
v.text = "Students"
return v
}()

let labelLesson: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
v.text = "Lesson"
return v
}()

let divider: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .gray
return v
}()

override func viewDidLoad() {
super.viewDidLoad()

view.addSubview(divider)
NSLayoutConstraint.activate([
divider.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20.0),
divider.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20.0),
divider.heightAnchor.constraint(equalToConstant: 2.0),
divider.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0),
])

let sv = UIStackView()
sv.axis = .horizontal
sv.alignment = .fill
sv.spacing = 0 //Config.Dimensions.horizontalSpacing
sv.distribution = .fill
sv.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(sv)
NSLayoutConstraint.activate([
sv.leadingAnchor.constraint(equalTo: divider.leadingAnchor, constant: 16.0),
sv.trailingAnchor.constraint(equalTo: divider.trailingAnchor, constant: -16.0),
sv.topAnchor.constraint(equalTo: divider.bottomAnchor, constant: 8.0),
])

let viewCountStudent = UIView()
viewCountStudent.addSubview(studentCount)
viewCountStudent.addSubview(labelStudent)
studentCount.topAnchor.constraint(equalTo: viewCountStudent.topAnchor).isActive = true
studentCount.leftAnchor.constraint(equalTo: viewCountStudent.leftAnchor).isActive = true
studentCount.bottomAnchor.constraint(equalTo: viewCountStudent.bottomAnchor).isActive = true
labelStudent.topAnchor.constraint(equalTo: viewCountStudent.topAnchor).isActive = true
labelStudent.leftAnchor.constraint(equalTo: studentCount.rightAnchor, constant: 8.0).isActive = true
labelStudent.rightAnchor.constraint(equalTo: viewCountStudent.rightAnchor).isActive = true
labelStudent.bottomAnchor.constraint(equalTo: viewCountStudent.bottomAnchor).isActive = true

let viewCountLesson = UIView()
viewCountLesson.addSubview(lessonCount)
viewCountLesson.addSubview(labelLesson)
lessonCount.leftAnchor.constraint(equalTo: viewCountLesson.leftAnchor).isActive = true
lessonCount.topAnchor.constraint(equalTo: viewCountLesson.topAnchor).isActive = true
lessonCount.bottomAnchor.constraint(equalTo: viewCountLesson.bottomAnchor).isActive = true
labelLesson.leftAnchor.constraint(equalTo: lessonCount.rightAnchor, constant: 8.0).isActive = true
labelLesson.rightAnchor.constraint(equalTo: viewCountLesson.rightAnchor).isActive = true
labelLesson.topAnchor.constraint(equalTo: viewCountLesson.topAnchor).isActive = true
labelLesson.bottomAnchor.constraint(equalTo: viewCountLesson.bottomAnchor).isActive = true

let sp1 = spacerView()
let sp2 = spacerView()
let sp3 = spacerView()

sv.addArrangedSubview(sp1)
sv.addArrangedSubview(viewCountLesson)
sv.addArrangedSubview(sp2)
sv.addArrangedSubview(viewCountStudent)
sv.addArrangedSubview(sp3)

NSLayoutConstraint.activate([
sp2.widthAnchor.constraint(equalTo: sp1.widthAnchor, multiplier: 1.0),
sp3.widthAnchor.constraint(equalTo: sp1.widthAnchor, multiplier: 1.0),
])

[sp1, sp2, sp3, viewCountLesson, viewCountStudent, studentCount, labelStudent, lessonCount, labelLesson].forEach {
// set borderWidth to 1 to add borders to labels and views so we can see them
$0.layer.borderWidth = 0
$0.layer.borderColor = UIColor.lightGray.cgColor
// un-comment next line to set backgrounds to clear
//$0.backgroundColor = .clear
}

}

func spacerView() -> UIView {

let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .green
return v

}

}

Is there any proper way to fill equally labels in stack view?

Solved my problem by putting label into the view container.

Why does a UIStackView with a single view, fill Proportionally, and Layout Margins causes ambiguous constraint error?

Editing with clarification because my original answer was not entirely correct...

First, the .fillProportionally distribution property of a UIStackView is very often misunderstood.

Second, we get some oddities when a stack view's distribution is .fillProportionally and the stack view's .spacing is not 0, or when the stack view also has .layoutMargins set.

The problem you're hitting is the way auto-layout calculates the proportional sizing.

Based on experimentation, auto-layout calculates the proportional widths of the views and then applies the layout margins, subtracting from the width of the last view to accommodate the space.

This can be easily demonstrated as follows:

Sample Image

There are 6 horizontal stack views, each set to 200-pts wide, Distribution set to .fillProportionally, and filled with either one or two views. The Red views have an intrinsic width of 25, the Green views 75.

The first stack view, with a single view and no layout margins, fills the width as expected... the red view takes up 100% of the space.

The second stack view, with two views and no layout margins, also fills as expected... the red view is 50-pts wide (25%) and the green view is 150-pts wide (75%).

The third stack view, though, starts showing the problem. The single view is give a proportional width of 100%, or 200-pts... but then the layout margins are applied. This shifts the view 10-pts from the left, but because auto-layout doesn't subtract space from the first subview, it actually extends 10-pts past the edge of the stack view (so the red view is still 200-pts wide).

The fourth stack view looks like it's doing what we want... proportional fill with 10-pts margin on each side... the Red view is 50-pts wide (25% of 200) but the Green view is only 130-pts wide. So auto-layout gave the two views 50-pts (25%) and 150-pts (75%) but then it applied the margins and took the 20-pts away from the Green view.

Using layout margins of left: 100 right: 0 or left: 0 right: 100 for the bottom two stack views makes it much more obvious. Again, for each of those, Red gets 50-pts (25%) and Green gets 150-pts (75%), but then the margin of 100-pts gets stripped from Green.


So, to answer the original question about why we get ambiguous constraints when we have a single arranged subview and layout margins, look at stack view 3. Auto-layout could not manage to give Red 100% of the space and apply margins, so it throws the layout error.


Here's the code to run the above example. If you comment-out the third stack view, you won't get the error:

class ProportionalStackExampleViewController: UIViewController {

let outerStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.spacing = 8
return v
}()

let outerStackFrame: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.layer.borderWidth = 0.5
v.layer.borderColor = UIColor.blue.cgColor
return v
}()

let infoLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.font = UIFont.systemFont(ofSize: 12.0, weight: .light)
v.numberOfLines = 0
v.textAlignment = .center
v.text = "Red views have intrinsic width of 25\nGreen views have intrinsic width of 75\nAll horizontal stack views are 200-pts wide\nTap any view to see its width"
return v
}()

let sizeLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.text = "(width)"
return v
}()

let myGreen = UIColor(red: 0, green: 0.75, blue: 0, alpha: 1.0)

override func viewDidLoad() {
super.viewDidLoad()

for _ in 1...6 {
let lbl = UILabel()
lbl.font = UIFont.systemFont(ofSize: 12.0, weight: .light)
lbl.numberOfLines = 0
lbl.textAlignment = .center
outerStackView.addArrangedSubview(lbl)
let sv = UIStackView()
sv.translatesAutoresizingMaskIntoConstraints = false
sv.axis = .horizontal
sv.distribution = .fillProportionally
sv.spacing = 0
outerStackView.addArrangedSubview(sv)
}

view.addSubview(infoLabel)
view.addSubview(sizeLabel)
view.addSubview(outerStackFrame)
view.addSubview(outerStackView)

let g = view.safeAreaLayoutGuide

NSLayoutConstraint.activate([

infoLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),

sizeLabel.topAnchor.constraint(equalTo: infoLabel.bottomAnchor, constant: 20.0),
sizeLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),

outerStackView.topAnchor.constraint(equalTo: sizeLabel.bottomAnchor, constant: 20.0),
outerStackView.widthAnchor.constraint(equalToConstant: 200.0),
outerStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),

outerStackFrame.widthAnchor.constraint(equalTo: outerStackView.widthAnchor),
outerStackFrame.heightAnchor.constraint(equalTo: outerStackView.heightAnchor),
outerStackFrame.centerXAnchor.constraint(equalTo: outerStackView.centerXAnchor),
outerStackFrame.centerYAnchor.constraint(equalTo: outerStackView.centerYAnchor),
])

// StackView 1
if let lbl = outerStackView.arrangedSubviews[0] as? UILabel,
let sv = outerStackView.arrangedSubviews[1] as? UIStackView {

lbl.text = "One view, no layoutMargins"

var v = ProportionalView()
v.w = 25.0
v.backgroundColor = .red
sv.addArrangedSubview(v)

var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)

}

// StackView 2
if let lbl = outerStackView.arrangedSubviews[2] as? UILabel,
let sv = outerStackView.arrangedSubviews[3] as? UIStackView {

lbl.text = "Two views, no layoutMargins"

var v = ProportionalView()
v.w = 25.0
v.backgroundColor = .red
sv.addArrangedSubview(v)

var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)

v = ProportionalView()
v.w = 75.0
v.backgroundColor = myGreen
sv.addArrangedSubview(v)

tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
}

// comment out this block to see the auto-layout error goes away
// StackView 3
if let lbl = outerStackView.arrangedSubviews[4] as? UILabel,
let sv = outerStackView.arrangedSubviews[5] as? UIStackView {

lbl.text = "One view\nlayoutMargins left: 10 right: 10"

var v = ProportionalView()
v.w = 25.0
v.backgroundColor = .red
sv.addArrangedSubview(v)

sv.isLayoutMarginsRelativeArrangement = true
sv.layoutMargins = .init(top: 0, left: 10, bottom: 0, right: 10)

var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)

}

// StackView 4
if let lbl = outerStackView.arrangedSubviews[6] as? UILabel,
let sv = outerStackView.arrangedSubviews[7] as? UIStackView {

lbl.text = "Two views\nlayoutMargins left: 10 right: 10"

var v = ProportionalView()
v.w = 25.0
v.backgroundColor = .red
sv.addArrangedSubview(v)

var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)

v = ProportionalView()
v.w = 75.0
v.backgroundColor = myGreen
sv.addArrangedSubview(v)

sv.isLayoutMarginsRelativeArrangement = true
sv.layoutMargins = .init(top: 0, left: 10, bottom: 0, right: 10)

tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
}

// StackView 5
if let lbl = outerStackView.arrangedSubviews[8] as? UILabel,
let sv = outerStackView.arrangedSubviews[9] as? UIStackView {

lbl.text = "layoutMargins left: 100 right: 0"

var v = ProportionalView()
v.w = 25.0
v.backgroundColor = .red
sv.addArrangedSubview(v)

var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)

v = ProportionalView()
v.w = 75.0
v.backgroundColor = myGreen
sv.addArrangedSubview(v)

sv.isLayoutMarginsRelativeArrangement = true
sv.layoutMargins = .init(top: 0, left: 100, bottom: 0, right: 0)

tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
}

// StackView 6
if let lbl = outerStackView.arrangedSubviews[10] as? UILabel,
let sv = outerStackView.arrangedSubviews[11] as? UIStackView {

lbl.text = "layoutMargins left: 0 right: 100"

var v = ProportionalView()
v.w = 25.0
v.backgroundColor = .red
sv.addArrangedSubview(v)

var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)

v = ProportionalView()
v.w = 75.0
v.backgroundColor = myGreen
sv.addArrangedSubview(v)

sv.isLayoutMarginsRelativeArrangement = true
sv.layoutMargins = .init(top: 0, left: 0, bottom: 0, right: 100)

tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
}

}

@objc func showWidth(_ sender: UITapGestureRecognizer) -> Void {
if let v = sender.view {
sizeLabel.text = "Width: \(v.frame.width)"
sizeLabel.textColor = v.backgroundColor
}
}

}

class ProportionalView: UIView {
var w: CGFloat = 1.0

override var intrinsicContentSize: CGSize {
return CGSize(width: w, height: 40.0)
}

}

Custom distribution for UIStackView

You'll need to weight each view using contentHuggingPriority.

Try making the red view's contentHuggingPriority higher than the violet.



Related Topics



Leave a reply



Submit