Search code examples
swiftautolayoutframeboundscgrect

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


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.


Solution

  • 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()
    }