Search code examples
iosswiftxcodeautolayoutuibezierpath

How to get center point (CGPoint) of a UIView that has been set using autolayout


So I haven't seen any other SO question about this. Basically, I have set a UIView with autolayout, programmatically.

The issue occurs when I try to create a UIBezierPath:

let circularPath = UIBezierPath(arcCenter: myView.center, radius: (91*(1.2))/2 - (8/2), startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)

The issue is that the myView.center prints out: 0.0, 0.0 since I did not set a CGRect .frame for the myView, and instead used autolayout (which is below):

NSLayoutConstraint.activate([
    myView.heightAnchor.constraint(equalToConstant: 91),
    myView.widthAnchor.constraint(equalToConstant: 91),
    myView.centerXAnchor.constraint(equalToSystemSpacingAfter: myButton.centerXAnchor, multiplier: 1),
    myView.centerYAnchor.constraint(equalToSystemSpacingBelow: myButton.centerYAnchor, multiplier: 1),
])

and also perhaps I should note that myView is added as a subview of myButton (though this shouldn't really matter..)

So how do I make this work? how could I get the .center (a type CGPoint) to use in the UIBezierPath? THANKS!


Solution

  • A few observations:

    1. The center property is where the center of the current view is within the coordinate system of the superview. That’s not likely what you intended. You want the CGPoint(x: myView.bounds.midX, y: myView.bounds.midY), which is the midpoint of the current view.

    2. Because you’re using constraints, you need to defer this creation of the path until after the constraints are applied, because that’s when the view’s bounds are set. The typical solution is to implement a UIButton subclass, and in that subclass you can implement layoutSubviews, and set the path there.

    For example, a simple circular button might be defined as follows:

    @IBDesignable
    class CircleButton: UIButton {
        let normalColor: UIColor = .red
        let highlightedColor: UIColor = .init(red: 0.5, green: 0, blue: 0, alpha: 1)
    
        lazy var shapeLayer: CAShapeLayer = {
            let shapeLayer = CAShapeLayer()
            shapeLayer.fillColor = normalColor.cgColor
            return shapeLayer
        }()
    
        override init(frame: CGRect = .zero) {
            super.init(frame: frame)
            configure()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            configure()
        }
    
        func configure() {
            layer.addSublayer(shapeLayer)
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            let center = CGPoint(x: bounds.midX, y: bounds.midY)
            let radius = min(bounds.height, bounds.width) / 2
            shapeLayer.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
        }
    
        override var isHighlighted: Bool {
            didSet {
                if isHighlighted {
                    shapeLayer.fillColor = highlightedColor.cgColor
                } else {
                    shapeLayer.fillColor = normalColor.cgColor
                }
            }
        }
    }
    

    The virtue of this approach is that:

    • Your view controller is not encumbered with lots of code for rendering of circles.

    • This will work regardless of size that is rendered (which can be useful if your constraints are not for a fixed size, but might respond to the orientation of the device, size classes, etc.).

    • If you want, because this is @IBDesignable, you can render it in your storyboards/NIBs.