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.
@rmaddy's comment is correct: The best way to handle this is to use a custom view to manage
trackLayer
andcircleShapeLayer
. Override the custom view'slayoutSubviews
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:
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 sendslayoutSublayers
to the layer. A layer handles this by default by sendinglayoutSublayersOfLayer:
to its delegate. Usually the delegate is theUIView
that owns the layer.By default, a
UIView
handleslayoutSublayersOfLayer:
by (among other things) sending three messages:UIView
sendsviewWillLayoutSubviews
to its view controller, if the view is owned by a view controller.UIView
sends itselflayoutSubviews
.UIView
sendsviewDidLayoutSubviews
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 sendsviewDidLayoutSubviews
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 itsviewDidLayoutSubviews
.There are two good places to put code that will run when C's frame has definitely been updated:
Override B's
layoutSubviews
. Since B is the direct superview of C, you can be sure that after B'slayoutSubviews
callssuper.layoutSubviews()
, C's frame has been updated.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:
layoutSubviews
. This will be called ifC
changes size. It won't necessarily be called if C changes position but stays the same size.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:
I just threw some rando colors but I think you get the point...