When are subviews completely, correctly laid out?

2020-06-29 05:58发布

I am trying to programmatically draw a circle progress view and center it within a subview circleView, which I have set up/constrained in the interface builder. However, I am not sure when circleView's final size and center will be accessible (I'm using auto layout), which I ultimately need to draw the circle. Here's the involved code:

@IBOutlet weak var circleView: UIView!

let circleShapeLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()

override func viewDidLoad() {
    super.viewDidLoad()
    // createCircle()
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    print(circleView.frame.size.width)
    createCircle()
}

func createCircle() {

    // Draw/modify circle

    let center = circleView.center

    // Where I need to use circleView's width/center
    let circularPath = UIBezierPath(arcCenter: center, radius: circleView.frame.size.width, startAngle: -CGFloat.pi/2, endAngle: 2 * CGFloat.pi, clockwise: true)

    trackLayer.path = circularPath.cgPath
    trackLayer.strokeColor = UIColor.lightGray.cgColor
    trackLayer.lineWidth = 10
    trackLayer.fillColor = UIColor.clear.cgColor

    circleView.layer.addSublayer(trackLayer)

    circleShapeLayer.path = circularPath.cgPath
    circleShapeLayer.strokeColor = Colors.tintColor.cgColor
    circleShapeLayer.lineWidth = 10
    circleShapeLayer.fillColor = UIColor.clear.cgColor
    circleShapeLayer.lineCap = kCALineCapRound
    // circleShapeLayer.strokeEnd = 0

    circleView.layer.addSublayer(circleShapeLayer)

}

This prints the width of circleView twice, and only on the second run of viewDidLayoutSubviews() is it actually correct:

207.0
187.5 // Correct (width of entire view is 375)

However, the circle draws incorrectly along the same exact path both times, which baffles me because the width changes as shown above. Maybe I'm thinking about this the wrong way?

I'd rather not draw the circle twice and was hoping there would a way to run createCircle() within viewDidLoad() instead, but at the moment this just gives me the same result. Any help would be very much appreciated.

2条回答
Anthone
2楼-- · 2020-06-29 06:08

@rmaddy's comment is correct: The best way to handle this is to use a custom view to manage trackLayer and circleShapeLayer. Override the custom view's layoutSubviews to set the frame and/or path of the layers.

That said, I'll answer your stated question of “When are subviews completely, correctly laid out?”

Consider this view hierarchy:

A
|
+--- B
|    |
|    +--- C
|    |
|    +--- D
|
+--- E
     |
     +--- F
     |
     +--- G

During the layout phase of the run loop, Core Animation traverses the layer hierarchy in depth-first order, looking for layers that need layout. So Core Animation visits A's layer first, then B's layer, then C's layer, then D's layer, then E's layer, then F's layer, then G's layer.

If a layer needs layout (its needsLayout property is true), then Core Animation sends layoutSublayers to the layer. A layer handles this by default by sending layoutSublayersOfLayer: to its delegate. Usually the delegate is the UIView that owns the layer.

By default, a UIView handles layoutSublayersOfLayer: by (among other things) sending three messages:

  1. The UIView sends viewWillLayoutSubviews to its view controller, if the view is owned by a view controller.
  2. The UIView sends itself layoutSubviews.
  3. The UIView sends viewDidLayoutSubviews to its view controller, if the view is owned by a view controller.

In the default implementation of -[UIView layoutSubviews], the view sets the frame of each of its direct subviews, based on auto layout constraints.

Note that in layoutSubviews, a view only sets the frames of its direct subviews. So for example, A only sets the frames of B and E. It does not set the frames of C, D, F, and G.

So let's suppose A is the view of a view controller, but none of the other views are owned by a view controller.

When A's handles layoutSubviews, it sets the frames of B and E. Then it sends viewDidLayoutSubviews to its view controller. The frames of C, D, F, and G have not been updated at this point. The view controller cannot assume that C, D, F, and G have correct frames in its viewDidLayoutSubviews.

There are two good places to put code that will run when C's frame has definitely been updated:

  1. Override B's layoutSubviews. Since B is the direct superview of C, you can be sure that after B's layoutSubviews calls super.layoutSubviews(), C's frame has been updated.

  2. Put a view controller in charge of B. That is, make B be the view of some view controller. Then, override viewDidLayoutSubviews in the view controller that owns B.

If you only need to know when C's size has definitely been updated, you have a third option:

  1. Override C's layoutSubviews. This will be called if C changes size. It won't necessarily be called if C changes position but stays the same size.
查看更多
家丑人穷心不美
3楼-- · 2020-06-29 06:26

Like others before, you should write the circle view as a standalone object . Just subclass UIView and call it each time you need it.

I took your code and got started:

import Foundation
import UIKit

class MyCircle : UIView {

    init(){
        super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        setupViews();
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }


    func setupViews() {
        let circleShapeLayer = CAShapeLayer()
        let trackLayer = CAShapeLayer()
        // Draw/modify circle
        let center = self.center

        // Where I need to use circleView's width/center
        let circularPath = UIBezierPath(arcCenter: center, radius: self.frame.size.width, startAngle: -CGFloat.pi/2, endAngle: 2 * CGFloat.pi, clockwise: true)

        trackLayer.path = circularPath.cgPath
        trackLayer.strokeColor = UIColor.lightGray.cgColor
        trackLayer.lineWidth = 10
        trackLayer.fillColor = UIColor.clear.cgColor

        self.layer.addSublayer(trackLayer)

        circleShapeLayer.path = circularPath.cgPath
        circleShapeLayer.strokeColor = UIColor.blue.cgColor
        circleShapeLayer.lineWidth = 10
        circleShapeLayer.fillColor = UIColor.clear.cgColor
        circleShapeLayer.lineCap = kCALineCapRound
        circleShapeLayer.strokeEnd = 0
        self.layer.addSublayer(circleShapeLayer)
        self.backgroundColor = UIColor.black
    }

}

I just threw some rando colors but I think you get the point...

查看更多
登录 后发表回答