Creating view with programmatic constraints causes the content to be drawn in the wrong place

Viewed 44

I am adding a custom view to another view who then has its constraints set programatically. This results in the content of the view being drawn in the wrong place, and I'm not sure why. Here's the code I have and what I tried so far: enter image description here

// Called in viewDidLoad
let buttonView = UIView()
buttonView.translatesAutoresizingMaskIntoConstraints = false

let ring = CircularProgressView()
ring.frame = CGRect(x: 0, y: 0, width: 80, height: 80)  
ring.backgroundColor = .yellow
    
buttonView.addSubview(ring)
self.view.addSubview(buttonView)


NSLayoutConstraint.activate([
    buttonView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
    buttonView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -28),
    buttonView.heightAnchor.constraint(equalToConstant: 80),
    buttonView.widthAnchor.constraint(equalToConstant: 80),
    
    ring.centerXAnchor.constraint(equalTo: buttonView.centerXAnchor),
    ring.centerYAnchor.constraint(equalTo: buttonView.centerYAnchor)
])

I tried calling ring.layoutIfNeeded() or setting the width and height of the ring view with constraints, and it didn't change anything.

ring.heightAnchor.constraint(equalToConstant: 80),
ring.widthAnchor.constraint(equalToConstant: 80)

The CircularProgressView class looks like this. I tried using bounds instead of frame here or not halving the values by 2 and again no change.

class CircularProgressView: UIView {

    // First create two layer properties
    private var circleLayer = CAShapeLayer()
    private var progressLayer = CAShapeLayer()
    private var currentValue: Double = 0

    override init(frame: CGRect) {
        super.init(frame: frame)
        createCircularPath()
    }

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


    func createCircularPath() {
        let circularPath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0),
                                        radius: 41,
                                        startAngle: -.pi / 2,
                                        endAngle: 3 * .pi / 2,
                                        clockwise: true)
        circleLayer.path = circularPath.cgPath
        circleLayer.fillColor = UIColor.clear.cgColor
        circleLayer.lineCap = .round
        circleLayer.lineWidth = 3.0
        circleLayer.strokeColor = UIColor.black.withAlphaComponent(0.2).cgColor
        progressLayer.path = circularPath.cgPath
        progressLayer.fillColor = UIColor.clear.cgColor
        progressLayer.lineCap = .round
        progressLayer.lineWidth = 3.0
        progressLayer.strokeEnd = 0
        progressLayer.strokeColor = UIColor.black.cgColor
        
        layer.addSublayer(circleLayer)
        layer.addSublayer(progressLayer)
        
        let circularProgressAnimation = CABasicAnimation(keyPath: "strokeEnd")
        circularProgressAnimation.duration = 2
        circularProgressAnimation.toValue = 1
        circularProgressAnimation.fillMode = .forwards
        circularProgressAnimation.isRemovedOnCompletion = false
        progressLayer.add(circularProgressAnimation, forKey: "progressAnim")
        progressLayer.pauseAnimation()
    }
}

This all works with no issue if done via the storyboard, but I need to create it programatically for this use case, and I'm a little confused about what's wrong.

2 Answers

You need to make both views programmatically like

// Called in viewDidLoad
let buttonView = UIView()
buttonView.translatesAutoresizingMaskIntoConstraints = false

let ring = CircularProgressView()
ring.translatesAutoresizingMaskIntoConstraints = false 
ring.backgroundColor = .yellow
    
buttonView.addSubview(ring)
self.view.addSubview(buttonView)


NSLayoutConstraint.activate([
    buttonView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
    buttonView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -28),
    buttonView.heightAnchor.constraint(equalToConstant: 80),
    buttonView.widthAnchor.constraint(equalToConstant: 80),
    
    ring.centerXAnchor.constraint(equalTo: buttonView.centerXAnchor),
    ring.centerYAnchor.constraint(equalTo: buttonView.centerYAnchor),
    ring.heightAnchor.constraint(equalToConstant: 80),
    ring.widthAnchor.constraint(equalToConstant: 80)
])

and Call createCircularPath only inside layoutSubviews

override func layoutSubviews() {
   super.layoutSubviews()
   createCircularPath()
}

Try to also set CircularProgressView's translatesAutoresizingMaskIntoConstraints property to false:

ring.translatesAutoresizingMaskIntoConstraints = false
Related