Search code examples
swiftcashapelayercaanimation

Can CAShapeLayer shapes center itself in a subView?


I created an arbitrary view

let middleView = UIView(
    frame: CGRect(x: 0.0,
                  y: view.frame.height/4,
                  width: view.frame.width,
                  height: view.frame.height/4))
middleView.backgroundColor = UIColor.blue
view.addSubview(middleView)

Then I created a circle using UIBezierPath; however when I set the position to middleView.center, the circle is far off to the bottom of the view. Can you set the position in the center of a subview?

iPhone 8 Plus with view and uncentered circle

let shapeLayer = CAShapeLayer()
let circlePath = UIBezierPath(
        arcCenter: .zero,
        radius: 100,
        startAngle: CGFloat(0).toRadians(),
        endAngle: CGFloat(360).toRadians(),
        clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.strokeColor = UIColor.purple.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.position = middleView.center
middleView.layer.addSublayer(shapeLayer)

How do I center this circle in that view?


Solution

  • You have two problems.

    First, you are setting shapeLayer.position = middleView.center. The center of a view is is the superview's geometry. In other words, middleView.center is relative to view, not to middleView. But then you're adding shapeLayer as a sublayer of middleView.layer, which means shapeLayer needs a position that is in middleView's geometry, not in view's geometry. You need to set shapeLayer.position to the center of middleView.bounds:

    shapeLayer.position = CGPoint(x: middleView.bounds.midX, y: middleView.bounds.midY)
    

    Second, you didn't say where you're doing all this. My guess is you're doing it in viewDidLoad. But that is too early. In viewDidLoad, the views loaded from the storyboard still have the frames they were given in the storyboard, and haven't been laid out for the current device's screen size yet. So it's a bad idea to look at frame (or bounds or center) in viewDidLoad if you don't do something to make sure that things will be laid out correctly during the layout phase. Usually you do this by setting the autoresizingMask or creating constraints. Example:

    let middleView = UIView(
        frame: CGRect(x: 0.0,
                      y: view.frame.height/4,
                      width: view.frame.width,
                      height: view.frame.height/4))
    middleView.backgroundColor = UIColor.blue
    middleView.autoresizingMask = [.flexibleWidth, .flexibleHeight, .flexibleTopMargin, .flexibleBottomMargin]
    view.addSubview(middleView)
    

    However, shapeLayer doesn't belong to a view, so it doesn't have an autoresizingMask and can't be constrained. You have to lay it out in code. You could do that, but it's better to just use a view to manage the shape layer. That way, you can use autoresizingMask or constraints to control the layout of the shape, and you can set it up in viewDidLoad.

        let circleView = CircleView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
        circleView.center = CGPoint(x: middleView.bounds.midX, y: middleView.bounds.midY)
        circleView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
        circleView.shapeLayer.strokeColor = UIColor.purple.cgColor
        circleView.shapeLayer.fillColor = nil
        middleView.addSubview(circleView)
    
    ...
    
    class CircleView: UIView {
        override class var layerClass: AnyClass { return CAShapeLayer.self }
    
        var shapeLayer: CAShapeLayer { return layer as! CAShapeLayer }
    
        override func layoutSubviews() {
            super.layoutSubviews()
            shapeLayer.path = UIBezierPath(ovalIn: bounds).cgPath
        }
    
    }
    

    Result:

    demo

    And after rotating to landscape:

    demo in landscape