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
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:
- The
UIView
sends viewWillLayoutSubviews
to its view controller, if the view is owned by a view controller.
- The
UIView
sends itself layoutSubviews
.
- 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:
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.
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:
- 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.
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...