2
votes

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?

3
Please show the code where you create and add the labels.jrturton
you never added constraints for the labelsSh_Khan
From the code posted, I can't see where labels are populated into your view so I'm not sure what they consist of or how they're initially configured (frame wise).Craig

3 Answers

1
votes

You need to call self.container.layoutIfNeeded() before the print lines

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, cornerRadii: CGSize(width: 20, height: 20))
            mask.append(maskBezierPath)
        }

        self.shapeLayer?.path = mask.cgPath

        self.container.layoutIfNeeded()  // here

        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: UILabel) -> UIRectCorner {
        switch label {
        case self.labels.first:
            return [.topLeft, .topRight, .bottomRight]
        case self.labels.last:
            return [.topRight, .bottomLeft, .bottomRight]
        default:
            return [.topRight, .bottomLeft]
        }
    }
    // endregion
}
1
votes

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.

0
votes

@available(iOS 6.0, *) open func updateConstraints() // Override this to adjust your special constraints during a constraints update pass

after super.updateConstraints() frames should have right sizes