Here's a complete example showing that layoutSubviews()
is called on all the arrangedSubviews
of a stack view... and all of their subviews.
Here's how it looks to start:

- the stack view has a "dashed outline"
- the "bottom" (green) view is our "Title" view - one multiline label in a
UIView
- the "top" (yellow) view is our "Content" view - two labels and a "round" view
Each time we tap the button, the text in the Title view will change:


The fact the "round view" remains round tells us its layoutSubviews()
(where we update the cornerRadius) is being called. If it wasn't, it would look like this:

Here's the code for this example (no @IBOutlet
or @IBAction
connections):
class StackExampleViewController: UIViewController {
let testButton: UIButton = {
let v = UIButton()
v.setTitle("Tap Me", for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .blue
return v
}()
let contentView: ContentView = {
let v = ContentView()
return v
}()
let titleView: TitleView = {
let v = TitleView()
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 8
return v
}()
let dashedView: DashedBorderView = {
let v = DashedBorderView()
return v
}()
let sampleData: [String] = [
"This is the Title View",
"A label can contain an arbitrary amount of text, but UILabel may shrink, wrap, or truncate the text, depending on the size of the bounding rectangle and properties you set.",
"You can control the font, text color, alignment, highlighting, and shadowing of the text in the label.",
"What's a UIButton?",
"You can set the title, image, and other appearance properties of a button. In addition, you can specify a different appearance for each button state."
]
var idx: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// standard auto-layout
[testButton, contentView, titleView, stackView, dashedView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
// add contentView and titleView to stackView
stackView.addArrangedSubview(contentView)
stackView.addArrangedSubview(titleView)
// add button, dashedView and stackView
// (dashedView will be used to show the frame of stackView)
view.addSubview(testButton)
view.addSubview(dashedView)
view.addSubview(stackView)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// testButton 40-pts from top, centeredX
testButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
testButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// stackView 40-pts from testButton
// 40-pts on each side
stackView.topAnchor.constraint(equalTo: testButton.bottomAnchor, constant: 40.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// stackView height = 300
stackView.heightAnchor.constraint(equalToConstant: 300.0),
// constrain dashedView centered to stackView
// width and height 2-pts greater (so we can "outline" the stackView frame)
dashedView.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: 2),
dashedView.heightAnchor.constraint(equalTo: stackView.heightAnchor, constant: 2),
dashedView.centerXAnchor.constraint(equalTo: stackView.centerXAnchor),
dashedView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor),
])
// add touchUp target
testButton.addTarget(self, action: #selector(self.didTap(_:)), for: .touchUpInside)
// this will track the text for updating titleView's label
idx = sampleData.count
updateText()
}
func updateText() -> Void {
// change the text in titleView's titleLabel
// let auto-layout handle ALL of the resizing
titleView.titleLabel.text = sampleData[idx % sampleData.count]
idx += 1
}
@objc func didTap(_ sender: Any) {
updateText()
}
}
class TitleView: UIView {
// TitleView has a multi-line UILabel
// with 20-pts "padding" on each side
// and 12-pts "padding" on top and bottom
var titleLabel: UILabel = {
let v = UILabel()
v.text = "Title Label"
v.numberOfLines = 0
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .green
[titleLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
$0.textAlignment = .center
addSubview($0)
}
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
])
// we want this label's text to control its height
titleLabel.setContentHuggingPriority(.required, for: .vertical)
}
override func layoutSubviews() {
super.layoutSubviews()
print(NSStringFromClass(type(of: self)), #function, bounds)
}
}
class ContentView: UIView {
// ContentView has two labels and a "round" view
var labelA: UILabel = {
let v = UILabel()
v.text = "Label A"
return v
}()
var labelB: UILabel = {
let v = UILabel()
v.text = "The Content View is Yellow"
return v
}()
var roundView: RoundView = {
let v = RoundView()
v.backgroundColor = .orange
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .yellow
[labelA, labelB].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.backgroundColor = .cyan
$0.textAlignment = .center
addSubview($0)
}
roundView.translatesAutoresizingMaskIntoConstraints = false
addSubview(roundView)
NSLayoutConstraint.activate([
// constrain labelA 20-pts from top / leading
labelA.topAnchor.constraint(equalTo: topAnchor, constant: 20.0),
labelA.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
// constrain roundView 20-pts from trailing
roundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
// constrain labelA trailing 20-pts from roundView leading
labelA.trailingAnchor.constraint(equalTo: roundView.leadingAnchor, constant: -20.0),
// constrain roundView height equal to lableA height
roundView.heightAnchor.constraint(equalTo: labelA.heightAnchor),
// keep roundView square (1:1 ratio)
roundView.widthAnchor.constraint(equalTo: roundView.heightAnchor),
// center roundView vertically to labelA
roundView.centerYAnchor.constraint(equalTo: labelA.centerYAnchor),
// labelB top is 8-pts below labelA
labelB.topAnchor.constraint(equalTo: labelA.bottomAnchor, constant: 8.0),
// constrain labelB 20-pts from leading / trailing / bottom
labelB.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
labelB.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
labelB.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20.0),
// keep labelA and labelB heights equal
labelA.heightAnchor.constraint(equalTo: labelB.heightAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
print(NSStringFromClass(type(of: self)), #function, bounds)
}
}
class RoundView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
print(NSStringFromClass(type(of: self)), #function, bounds)
// update cornerRadius here to keep it round
layer.cornerRadius = bounds.size.width * 0.5
}
}
class DashedBorderView: UIView {
// simple view with dashed border
var shapeLayer: CAShapeLayer!
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
shapeLayer = self.layer as? CAShapeLayer
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor(red: 1.0, green: 0.25, blue: 0.25, alpha: 1.0).cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.lineDashPattern = [8,8]
}
override func layoutSubviews() {
super.layoutSubviews()
print(NSStringFromClass(type(of: self)), #function, bounds)
shapeLayer.path = UIBezierPath(rect: bounds).cgPath
}
}
Note: the views also print() their class name and bounds when they receive calls to layoutSubviews()
, so you'll see something like this in debug console:
SampleApp.TitleView layoutSubviews() (0.0, 0.0, 295.0, 146.0)
SampleApp.ContentView layoutSubviews() (0.0, 0.0, 295.0, 146.0)
SampleApp.RoundView layoutSubviews() (0.0, 0.0, 49.0, 49.0)
SampleApp.TitleView layoutSubviews() (0.0, 0.0, 295.0, 105.5)
SampleApp.ContentView layoutSubviews() (0.0, 0.0, 295.0, 186.5)
SampleApp.RoundView layoutSubviews() (0.0, 0.0, 69.0, 69.5)