Reputation: 1885
While using a playground to play with UIStackView
programmatically that contains two labels, I realized that the way I'm doing the stack view and the labels don't calculate their sizes automatically. That's my (ugly) code:
import UIKit
let label1 = UILabel()
let label2 = UILabel()
let arrangedViews: [UIView] = [label1, label2]
let view = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
label1.text = "Text 1"
label2.text = "Text 2"
for case let label as UILabel in arrangedViews {
label.textColor = .white
label.sizeToFit()
}
view.backgroundColor = .black
let stackView = UIStackView(arrangedSubviews: arrangedViews)
stackView.axis = .vertical
stackView.distribution = .fill
stackView.layer.borderColor = UIColor.white.cgColor
stackView.layer.borderWidth = 1
var stackViewIdealSize = CGSize()
stackViewIdealSize.width = label1.intrinsicContentSize.width
arrangedViews.forEach{label in stackViewIdealSize.height += label.intrinsicContentSize.height}
stackView.frame.size = stackViewIdealSize
view.addSubview(stackView)
This code, besides being huge to do such a "simple thing", still calculates the size not in the right way, for my playground view is like this:
Is there a way to make UIStackView
or even UIView
s to be more smart about calculate the ideal dimensions?
Upvotes: 3
Views: 4923
Reputation: 385998
I'm surprised you're getting any effect from setting the stack view's borderColor
and borderWidth
. UIStackView
is documented to be “a nonrendering subclass of UIView
; that is, it does not provide any user interface of its own.” In fact it uses a CATransformLayer
as its layer, and the CATransformLayer
documentation says “The CALayer
properties that are rendered by a layer are ignored, including: backgroundColor
, contents
, border style properties, stroke style properties, etc.”.
Anyway, because its alignment
is .fill
, the stack view creates required-priority constraints that force its arranged subviews to be the same width as itself. Here are these constraints:
<NSLayoutConstraint:0x60800008c210 'UISV-alignment' UILabel:0x7f821e4023d0'Text 1'.leading == UILabel:0x7f821e608ef0'Text 2'.leading (active)>
<NSLayoutConstraint:0x60800008c2b0 'UISV-alignment' UILabel:0x7f821e4023d0'Text 1'.trailing == UILabel:0x7f821e608ef0'Text 2'.trailing (active)>
<NSLayoutConstraint:0x60800008c080 'UISV-canvas-connection' UIStackView:0x7f821e40b790.leading == UILabel:0x7f821e4023d0'Text 1'.leading (active)>
<NSLayoutConstraint:0x60800008c1c0 'UISV-canvas-connection' H:[UILabel:0x7f821e4023d0'Text 1']-(0)-| (active, names: '|':UIStackView:0x7f821e40b790 )>
Each arranged subview label itself has a constraint setting its own width to its intrinsic width. However, this constraint is not required-priority by default. Here are these constraints:
<NSContentSizeLayoutConstraint:0x6080000b7a00 UILabel:0x7fe385601720'Text 1'.width == 44.5 Hug:250 CompressionResistance:750 (active)>
<NSContentSizeLayoutConstraint:0x6180000abac0 UILabel:0x7fb809d0d810'Text 2'.width == 47 Hug:250 CompressionResistance:750 (active)>
This constraint has two priorities (“Hug” and “CompressionResistance”). Both are less than required priority. (Required priority is 1000.)
In the absence of other, higher-priority constraints, these constraints would make the stack view exactly wide enough to fit its widest arranged subview.
The critical step you omitted is that you did not set stackView.translatesAutoresizingMaskIntoConstraints = false
. Because you did not set it to false, the stack view has required-priority constraints setting its position and size to its existing frame
. You set its existing frame.size.width
to the intrinsic width of the “Text 1” label, but that is narrower than the intrinsic width of the “Text 2” label. So the stack view gives itself a required-priority width constraint that in turn also controls the width of the “Text 2” label, forcing it narrower than its intrinsic width, so it clips its text. Here's that constraint:
<NSLayoutConstraint:0x60800009a6d0 'UIView-Encapsulated-Layout-Width' UIStackView:0x7fb125c059d0.width == 44.5 (active)>
By turning off translatesAutoresizingMaskIntoConstraints
, you tell the stack view not to create constraints matching its existing frame. If you then use constraints to set the stack view's position, but not its size, the other constraints (described earlier) will be able to set its size to fit its arranged subviews.
So here's how I'd write the playground:
import UIKit
import PlaygroundSupport
let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view.backgroundColor = #colorLiteral(red: 0.9568627477, green: 0.6588235497, blue: 0.5450980663, alpha: 1)
PlaygroundPage.current.liveView = view
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
for i in 1 ... 2 {
let label = UILabel()
label.text = "Text \(i)"
label.textColor = .white
stackView.addArrangedSubview(label)
}
view.addSubview(stackView)
view.leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
view.topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
To outline the stack view, we can add another view:
let stackOutlineView = UIView()
stackOutlineView.translatesAutoresizingMaskIntoConstraints = false
stackOutlineView.backgroundColor = .clear
stackOutlineView.layer.borderWidth = 1
stackOutlineView.layer.borderColor = UIColor.black.cgColor
view.addSubview(stackOutlineView)
stackView.leadingAnchor.constraint(equalTo: stackOutlineView.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: stackOutlineView.trailingAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: stackOutlineView.topAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: stackOutlineView.bottomAnchor).isActive = true
Here's the result:
Upvotes: 9