I'm having trouble centering CAShapeLayer within a UIView. I've search and most solutions are from pre 2015 in Obj C or haven't been solved.
Attached is what the image looks like. When I inspect it, its inside the red view, but idk why its not centering. I've tried resizing it but still doesn't work.
let progressView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
//MARK: - ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(progressView)
progressView.anchor(top: nil, left: nil, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 200, height: 200)
progressView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
progressView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
setupCircleLayers()
}
var shapeLayer: CAShapeLayer!
private func setupCircleLayers() {
let trackLayer = createCircleShapeLayer(strokeColor: UIColor.rgb(red: 56, green: 25, blue: 49, alpha: 1), fillColor: #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1))
progressView.layer.addSublayer(trackLayer)
}
private func createCircleShapeLayer(strokeColor: UIColor, fillColor: UIColor) -> CAShapeLayer {
let centerpoint = CGPoint(x: progressView.frame.width / 2, y: progressView.frame.height / 2)
let circularPath = UIBezierPath(arcCenter: centerpoint, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
let layer = CAShapeLayer()
layer.path = circularPath.cgPath
layer.fillColor = fillColor.cgColor
layer.lineCap = kCALineCapRound
layer.position = progressView.center
return layer
}
As @ukim says, your problem is that you are trying to determine the position of your layer, based on views and their size before these are finite.
When you are in viewDidLoad
you don't know the size and final position of your views yet. You can add the progressView
alright but you can not be sure that its size or position are correct until viewDidLayoutSubviews
(documented here).
So, if I move your call to setupCircleLayers
to viewDidLayoutSubviews
and I change the centerpoint to CGPoint.zero
and alter the calculation of your layer.position
to this:
layer.position = CGPoint(x: progressView.frame.size.width / 2, y: progressView.frame.size.height / 2)
Then I see this:
Which I hope is more what you were aiming for.
Here is the complete listing (note that I had to change some of your methods as I didn't have access to anchor
or UIColor.rgb
for instance but you can probably work your way around that :))
import UIKit
class ViewController: UIViewController {
var shapeLayer: CAShapeLayer!
let progressView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(progressView)
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.heightAnchor.constraint(equalToConstant: 200).isActive = true
progressView.widthAnchor.constraint(equalToConstant: 200).isActive = true
progressView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
progressView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if shouldAddSublayer {
setupCircleLayers()
}
}
private func setupCircleLayers() {
let trackLayer = createCircleShapeLayer(strokeColor: UIColor.init(red: 56/255, green: 25/255, blue: 49/255, alpha: 1), fillColor: #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1))
progressView.layer.addSublayer(trackLayer)
}
private var shouldAddSublayer: Bool {
/*
check if:
1. we have any sublayers at all, if we don't then its safe to add a new, so return true
2. if there are sublayers, see if "our" layer is there, if it is not, return true
*/
guard let sublayers = progressView.layer.sublayers else { return true }
return sublayers.filter({ $0.name == "myLayer"}).count == 0
}
private func createCircleShapeLayer(strokeColor: UIColor, fillColor: UIColor) -> CAShapeLayer {
let centerpoint = CGPoint.zero
let circularPath = UIBezierPath(arcCenter: centerpoint, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
let layer = CAShapeLayer()
layer.path = circularPath.cgPath
layer.fillColor = fillColor.cgColor
layer.lineCap = kCALineCapRound
layer.position = CGPoint(x: progressView.frame.size.width / 2, y: progressView.frame.size.height / 2)
layer.name = "myLayer"
return layer
}
}
Hope that helps.
Caveat
When you do the above, that also means that every time viewDidLayoutSubviews
is called, you are adding a new layer. To circumvent that, you can use the name
property of a layer
layer.name = "myLayer"
and then check if you have already added your layer. Something like this should work:
private var shouldAddSublayer: Bool {
/*
check if:
1. we have any sublayers at all, if we don't then its safe to add a new, so return true
2. if there are sublayers, see if "our" layer is there, if it is not, return true
*/
guard let sublayers = progressView.layer.sublayers else { return true }
return sublayers.filter({ $0.name == "myLayer"}).count == 0
}
Which you then use here:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if shouldAddSublayer {
setupCircleLayers()
}
}
I've updated the listing.
You are calling setupCircleLayers()
in viewDidLoad()
. At the time, progressView.frame
has not been calculated from the constraints yet.
Try
let centerpoint = CGPoint(x: 100, y: 100)
instead of calculating the value from progressView.frame
You can try this in your playground:
import UIKit
import PlaygroundSupport
class MyVC: UIViewController {
let progressView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
var layer: CAShapeLayer?
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(progressView)
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.backgroundColor = .red
progressView.widthAnchor.constraint(equalToConstant: 200).isActive = true
progressView.heightAnchor.constraint(equalToConstant: 200).isActive = true
progressView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
progressView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
setupCircleLayers()
}
var shapeLayer: CAShapeLayer!
private func setupCircleLayers() {
let trackLayer = createCircleShapeLayer(strokeColor: .red, fillColor: #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1))
progressView.layer.addSublayer(trackLayer)
}
private func createCircleShapeLayer(strokeColor: UIColor, fillColor: UIColor) -> CAShapeLayer {
let centerpoint = CGPoint(x: 100, y: 100)
let circularPath = UIBezierPath(arcCenter: centerpoint, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
let layer = CAShapeLayer()
layer.path = circularPath.cgPath
layer.fillColor = fillColor.cgColor
layer.lineCap = kCALineCapRound
return layer
}
}
let containerView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 375.0, height: 667.0))
let vc = MyVC()
PlaygroundPage.current.liveView = vc.view