2

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.

gcharita
  • 7,729
  • 3
  • 20
  • 37
CristianMoisei
  • 2,071
  • 2
  • 22
  • 28

2 Answers2

2

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()
}
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87
  • Thanks @Sh_Khan, but those two are used to align the parent in the safe area. So commenting them out just does this: https://ibb.co/4JyyLgZ – CristianMoisei Oct 18 '20 at 13:47
  • 1
    @CristianMoisei try the edit you need to make both views with constraints to be able to add workable relational constraints between them – Shehata Gamal Oct 18 '20 at 13:50
  • Afraid that doesn't work either. The result stays the same. I think there's something wrong with how the circle gets drawn and how the frame or bounds is placed. But I don't understand how that works well enough to figure it out. – CristianMoisei Oct 18 '20 at 13:53
  • Amazing, drawing the circle in layoutSubviews is what fixed it. translatesAutoresizingMaskIntoConstraints doesn't seem to make any difference. – CristianMoisei Oct 18 '20 at 13:57
1

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

ring.translatesAutoresizingMaskIntoConstraints = false
gcharita
  • 7,729
  • 3
  • 20
  • 37
  • Thanks @gcharita, but I'm afraid that doesn't change anything. I tried setting it both before and after it gets added to the parent view, if that makes any difference. – CristianMoisei Oct 18 '20 at 13:49