I've got a custom UIView subclass where I'm adding a couple of subviews programmatically. I'm setting up all the layout code with AutoLayout.
The problem comes when I override my UIView's layoutSubviews()
method to try to get my subview frames, as they always return .zero
as their frame.
However, If I go to the View Hiearchy Debugger in XCode, all the frames are calculated and shown correctly.
Here is the console output I log inside the layoutSubviews()
method:
layoutSubviews(): <PrologueTextView: 0x7fa50961abc0; frame = (19.75 -19.5; 335.5 168); clipsToBounds = YES; autoresize = RM+BM; layer = <CAShapeLayer: 0x60000022be20>>
layoutSubviews(): <Label: 0x7fa509424c60; baseClass = UILabel; frame = (0 0; 0 0); text = 'This is'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x60400028d160>>
layoutSubviews(): <Label: 0x7fa509424f60; baseClass = UILabel; frame = (0 0; 0 0); text = 'some sample'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x60400028d2a0>>
layoutSubviews(): <Label: 0x7fa509425260; baseClass = UILabel; frame = (0 0; 0 0); text = 'text for you'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x60400028d3e0>>
And here is my UIView subclass relevant code:
internal class PrologueTextView: UIView {
internal var labels: [UILabel] = []
internal let container: UIVisualEffectView = UIVisualEffectView()
// region #Properties
internal var shapeLayer: CAShapeLayer? {
return self.layer as? CAShapeLayer
}
internal override class var layerClass: AnyClass {
return CAShapeLayer.self
}
// endregion
// region #Initializers
internal override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
internal required init?(coder: NSCoder) {
super.init(coder: coder)
self.setup()
}
// endregion
// region #UIView lifecycle
internal override func layoutSubviews() {
super.layoutSubviews()
let mask: UIBezierPath = UIBezierPath()
for label in self.labels {
let roundedCorners = self.roundedCorners(for: label)
let maskBezierPath = UIBezierPath(roundedRect: label.frame, byRoundingCorners: roundedCorners, cornerRadius: 4.0)
mask.append(maskBezierPath)
}
self.shapeLayer?.path = mask.cgPath
print("layoutSubviews(): \(self)")
print("layoutSubviews(): \(labels[0])")
print("layoutSubviews(): \(labels[1])")
print("layoutSubviews(): \(labels[2])")
}
// endregion
// region #Helper methods
private func setup() {
self.setupSubviews()
self.setupSubviewsAnchors()
}
private func setupSubviews() {
self.container.effect = UIBlurEffect(style: .light)
self.container.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.container)
let someSampleText = "This is\nsome sample\ntext for you"
for paragraph in someSampleText.components(separatedBy: "\n") {
let label = UILabel()
label.text = paragraph
label.translatesAutoresizingMaskIntoConstraints = false
self.labels.append(label)
self.container.contentView.addSubview(label)
}
}
private func setupSubviewsAnchors() {
NSLayoutConstraint.activate([
self.container.topAnchor.constraint(equalTo: self.topAnchor),
self.container.bottomAnchor.constraint(equalTo: self.bottomAnchor),
self.container.leadingAnchor.constraint(equalTo: self.leadingAnchor),
self.container.trailingAnchor.constraint(equalTo: self.trailingAnchor)
])
for (index, label) in self.labels.enumerated() {
let offset = 16.0 * CGFloat(index)
if index == 0 {
label.topAnchor.constraint(equalTo: self.container.contentView.topAnchor).isActive = true
} else {
let prev = self.labels[index - 1]
label.topAnchor.constraint(equalTo: prev.bottomAnchor).isActive = true
if index == self.labels.count - 1 {
label.bottomAnchor.constraint(equalTo: self.container.contentView.bottomAnchor).isActive = true
}
}
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: self.container.leadingAnchor, constant: offset),
label.trailingAnchor.constraint(lessThanOrEqualTo: self.container.trailingAnchor)])
}
}
private func roundedCorners(for label: Label) -> UIRectCorner {
switch label {
case self.labels.first:
return [.topLeft, .topRight, .bottomRight]
case self.labels.last:
return [.topRight, .bottomLeft, .bottomRight]
default:
return [.topRight, .bottomLeft]
}
}
// endregion
}
So, is there any UIView method that gets called after AutoLayout has computed and set the frames for the view and it's subviews?
After battling around with it I've figured out what was happening.
layoutSubviews()
is indeed the way to go;
It will calculate the frames of the view and it's direct subviews.
The problem here is that the UILabels are not subviews of the main UIView subclass, but rather are subviews of the main UIView's subview (effect view).
Your view hierarchy example:
--> TestView
--> EffectView
--> UILabel
--> UILabel
--> UILabel
As you can see, layoutSubviews()
will give you the correct frames for TestView
and EffectView
since it's its direct subview, but won't give you the calculated frames from UILabels
because they aren't direct subviews of TestView
.