Adding a CAShapeLayer around a button - ios

I created a CAShapeLayer in the shape of a circle, and I want to add it around I button i have in the view. I am doing this instead of a border, due to animation purposes. I don't want a border around the button, I'd rather have a shape. This is how I am adding it, but for some reason, it is not adding the shape directly around the button.
This is my code to add the layer
recordLine = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: recordButton.center, radius: recordButton.frame.width / 2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
recordLine.path = circularPath.cgPath
recordLine.strokeColor = UIColor.white.cgColor
recordLine.fillColor = UIColor.clear.cgColor
recordLine.lineWidth = 5
view.layer.addSublayer(recordLine)
This is how it is adding the line for some reason.

This is happening because you are adding Shape layer before rendering the autolayout Constrain properly.
Please add a single line before adding shape layer : self.view.layoutIfNeeded()
#IBOutlet weak var roundButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
roundButton.layer.cornerRadius = 50.0
roundButton.clipsToBounds = true
roundButton.alpha = 0.5
self.view.layoutIfNeeded()
let recordLine = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: roundButton.center, radius: roundButton.frame.width / 2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: false)
recordLine.path = circularPath.cgPath
recordLine.strokeColor = UIColor.black.cgColor
recordLine.fillColor = UIColor.clear.cgColor
recordLine.lineWidth = 10
view.layer.addSublayer(recordLine)
}
Please check the reference image
This is working for me.

My button is programmatic. I had to add the the cashapelayer in viewDidLayoutSubviews
lazy var cameraButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = UIColor.lightGray
return button
}()
var wasCAShapeLayerAddedToCameraButton = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !wasCAShapeLayerAddedToCameraButton {
wasCAShapeLayerAddedToCameraButton = true
cameraButton.layer.cornerRadius = cameraButton.frame.width / 2
addCAShapeLayerToCameraButton()
}
}
func addCAShapeLayerToCameraButton() {
let circularPath = UIBezierPath(arcCenter: cameraButton.center,
radius: (cameraButton.frame.width / 2),
startAngle: 0,
endAngle: 2 * .pi,
clockwise: true)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 10
view.layer.addSublayer(shapeLayer)
}

// try like this i hope it will work for you.
Note: Color and other properties change as your requirement
let shapeLayer = CAShapeLayer()
shapeLayer.backgroundColor = UIColor.clear.cgColor
shapeLayer.name = "Star"
let path: CGPath = UIBezierPath(arcCenter: recordButton.center, radius: recordButton.frame.width / 2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = path
shapeLayer.lineWidth = 5.0
self.layer.mask = shapeLayer

Related

How to draw a circle using CAShapeLayer to be centered in a custom UIView in swift programmatically

I am trying to draw a circular progress bar using CAShapeLayer inside a custom UIView which has been auto constraint, I don't want to draw my circle in the center of my super view but rather in the center of my custom view because I have other views on top my code below draws a circle but it is not positioned in the specified view
// Custom View
let gaugeViewHolder = UIView()
scrollView.addSubview(gaugeViewHolder)
gaugeViewHolder.translatesAutoresizingMaskIntoConstraints = false
gaugeViewHolder.backgroundColor = UIColor.black
gaugeViewHolder.leadingAnchor.constraint(equalTo: motherView.leadingAnchor).isActive = true
gaugeViewHolder.topAnchor.constraint(equalTo: defaultAccImage.bottomAnchor, constant: 70).isActive = true
gaugeViewHolder.trailingAnchor.constraint(equalTo: motherView.trailingAnchor).isActive = true
gaugeViewHolder.heightAnchor.constraint(equalToConstant: 200).isActive = true
//Now my circle
let shapeLayer = CAShapeLayer()
let centerForGauge = gaugeViewHolder.center
let circularPath = UIBezierPath(arcCenter: centerForGauge
, radius: 80, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.white.withAlphaComponent(0.20).cgColor
shapeLayer.lineWidth = 10
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
gaugeViewHolder.layer.addSublayer(shapeLayer)
let gaugeViewHolder = UIView()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.addSubview(gaugeViewHolder)
gaugeViewHolder.translatesAutoresizingMaskIntoConstraints = false
gaugeViewHolder.backgroundColor = UIColor.black
gaugeViewHolder.leadingAnchor.constraint(equalTo: motherView.leadingAnchor).isActive = true
gaugeViewHolder.topAnchor.constraint(equalTo: defaultAccImage.bottomAnchor, constant: 70).isActive = true
gaugeViewHolder.trailingAnchor.constraint(equalTo: motherView.trailingAnchor).isActive = true
gaugeViewHolder.heightAnchor.constraint(equalToConstant: 200).isActive = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let shapeLayer = CAShapeLayer()
let centerForGauge = gaugeViewHolder.center
print("gauge width:: \(centerForGauge)")
let circularPath = UIBezierPath(arcCenter: CGPoint(x: gaugeViewHolder.frame.size.width/2, y: gaugeViewHolder.frame.size.height/2)
, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.white.withAlphaComponent(0.50).cgColor
shapeLayer.lineWidth = 10
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
gaugeViewHolder.layer.addSublayer(shapeLayer)
}
You may consider add the layer later after all constraints has been applied to your view if you don't set frame by yourself at design time. This works as I have tested in an example.
var gaugeViewHolder : UIView!
override func viewDidLoad() {
super.viewDidLoad()
gaugeViewHolder = UIView()
scrollView.addSubview(gaugeViewHolder)
gaugeViewHolder.translatesAutoresizingMaskIntoConstraints = false
gaugeViewHolder.backgroundColor = UIColor.black
gaugeViewHolder.leadingAnchor.constraint(equalTo: motherView.leadingAnchor).isActive = true
gaugeViewHolder.topAnchor.constraint(equalTo: defaultAccImage.bottomAnchor, constant: 70).isActive = true
gaugeViewHolder.trailingAnchor.constraint(equalTo: motherView.trailingAnchor).isActive = true
gaugeViewHolder.heightAnchor.constraint(equalToConstant: 200).isActive = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let shapeLayer = CAShapeLayer()
let centerForGauge = gaugeViewHolder.center
let circularPath = UIBezierPath(arcCenter: centerForGauge
, radius: 80, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.white.withAlphaComponent(0.20).cgColor
shapeLayer.lineWidth = 10
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = CAShapeLayerLineCap.round
gaugeViewHolder.layer.addSublayer(shapeLayer)
}
You never set the frame of the shape layer. It should be the owning view's bounds rect if you want the shape layer to overlay the view's rectangle.
Here is code that adds a shape layer to a view that I added in a sample app storyboard and wired up as an IBOutlet:
#IBOutlet weak var gaugeViewHolder: UIView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
gaugeViewHolder.backgroundColor = UIColor.lightGray
//Now my circle
let shapeLayer = CAShapeLayer()
shapeLayer.borderWidth = 2.0 //Add a box on the shape layer so you can see where it gets drawn.
shapeLayer.frame = gaugeViewHolder.bounds //Use the view's bounds as the layer's frame
//Convert gaugeViewHolder's center from it's superview's coordinate system to it's coordinate system
let centerForGauge = gaugeViewHolder?.superview?.convert(gaugeViewHolder.center, to: gaugeViewHolder) ?? CGPoint.zero
let lineWidth = CGFloat(5.0)
//Use 1/2 the shortest side of the shapeLayer's frame for the radius, inset for the circle path's thickness.
let radius = max(shapeLayer.frame.size.width, shapeLayer.frame.size.height)/2.0 - lineWidth / 2.0
let circularPath = UIBezierPath(arcCenter: centerForGauge
, radius: radius, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = lineWidth
shapeLayer.fillColor = UIColor.yellow.cgColor
shapeLayer.lineCap = .round
gaugeViewHolder.layer.addSublayer(shapeLayer)
}
I changed the colors and alpha around, and added a borderWidth to the shape layer to make everything stand out.

Creating a thin black circle (unfilled) within a filled white circle (UIButton)

I'm trying to replicate the default camera button on iOS devices:
I'm able to create a white circular button with black button within it. However, the black button is also filled, instead of just being a thin circle.
This is what I have (most of it has been copied from different sources and put together, so the code isn't efficient)
The object represents the button,
func applyRoundCorner(_ object: AnyObject) {
//object.layer.shadowColor = UIColor.black.cgColor
//object.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
object.layer.cornerRadius = (object.frame.size.width)/2
object.layer.borderColor = UIColor.black.cgColor
object.layer.borderWidth = 5
object.layer.masksToBounds = true
//object.layer.shadowRadius = 1.0
//object.layer.shadowOpacity = 0.5
var CircleLayer = CAShapeLayer()
let center = CGPoint (x: object.frame.size.width / 2, y: object.frame.size.height / 2)
let circleRadius = object.frame.size.width / 6
let circlePath = UIBezierPath(arcCenter: center, radius: circleRadius, startAngle: CGFloat(M_PI), endAngle: CGFloat(M_PI * 2), clockwise: true)
CircleLayer.path = circlePath.cgPath
CircleLayer.strokeColor = UIColor.black.cgColor
//CircleLayer.fillColor = UIColor.blue.cgColor
CircleLayer.lineWidth = 1
CircleLayer.strokeStart = 0
CircleLayer.strokeEnd = 1
object.layer.addSublayer(CircleLayer)
}
Basic Approach
You could do it like this (for the purpose of demonstration, I would do the button programmatically, using a playground):
let buttonWidth = 100.0
let button = UIButton(frame: CGRect(x: 0, y: 0, width: buttonWidth, height: buttonWidth))
button.backgroundColor = .white
button.layer.cornerRadius = button.frame.width / 2
Drawing Part:
So, after adding the button and do the desired setup (make it circular), here is part of how you could draw a circle in it:
let circlePath = UIBezierPath(arcCenter: CGPoint(x: buttonWidth / 2,y: buttonWidth / 2), radius: 40.0, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.black.cgColor
circleLayer.lineWidth = 2.5
// adding the layer into the button:
button.layer.addSublayer(circleLayer)
Probably, circleLayer.fillColor = UIColor.clear.cgColor is the part you missing 🙂.
Therefore:
Back to your case:
Aside Bar Tip:
For implementing applyRoundCorner, I would suggest to let it has only the job for rounding the view, and then create another function to add the circle inside the view. And that's for avoiding any naming conflict, which means that when reading "applyRoundCorner" I would not assume that it is also would add circle to my view! So:
func applyRoundedCorners(for view: UIView) {
view.layer.cornerRadius = view.frame.size.width / 2
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 5.0
view.layer.masksToBounds = true
}
func drawCircle(in view: UIView) {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: view.frame.size.width / 2,y: view.frame.size.width / 2),
radius: view.frame.size.width / 2.5,
startAngle: 0,
endAngle: CGFloat.pi * 2,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 2.5
button.layer.addSublayer(shapeLayer)
}
and now:
applyRoundedCorners(for: button)
drawCircle(in: button)
That's seems to be better. From another aspect, consider that you want to make a view to be circular without add a circle in it, with separated methods you could simply applyRoundedCorners(for: myView) without the necessary of adding a circle in it.
Furthermore:
As you can see, I changed AnyObject to UIView, it seems to be more logical to your case. So here is a cool thing that we could do:
extension UIView {
func applyRoundedCorners(for view: UIView) {
view.layer.cornerRadius = view.frame.size.width / 2
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 5.0
view.layer.masksToBounds = true
}
func drawCircle(in view: UIView) {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: view.frame.size.width / 2,y: view.frame.size.width / 2),
radius: view.frame.size.width / 2.5,
startAngle: 0,
endAngle: CGFloat.pi * 2,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 2.5
button.layer.addSublayer(shapeLayer)
}
}
Now both applyRoundedCorners and drawCircle are implicitly included to the UIView (which means UIButton), instead of passing the button to these functions, you would be able to:
button.applyRoundedCorners()
button.drawCircle()
You just need to add circle Shape layer with lesser width and height
Try this code
func applyRoundCorner(_ object: UIButton) {
object.layer.cornerRadius = (object.frame.size.width)/2
object.layer.borderColor = UIColor.black.cgColor
object.layer.borderWidth = 5
object.layer.masksToBounds = true
let anotherFrame = CGRect(x: 12, y: 12, width: object.bounds.width - 24, height: object.bounds.height - 24)
let circle = CAShapeLayer()
let path = UIBezierPath(arcCenter: object.center, radius: anotherFrame.width / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)
circle.path = path.cgPath
circle.strokeColor = UIColor.black.cgColor
circle.lineWidth = 1.0
circle.fillColor = UIColor.clear.cgColor
object.layer.addSublayer(circle)
}
Note: Change frame value according to your requirements and best user experience
Output
I have no doubt there are a million different ways to approach this problem, this is just one...
I started with a UIButton for simplicity and speed, I might consider actually starting with a UIImage and simply setting the image properties of the button, but it would depend a lot on what I'm trying to achieve
internal extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
class RoundButton: UIButton {
override func draw(_ rect: CGRect) {
makeButtonImage()?.draw(at: CGPoint(x: 0, y: 0))
}
func makeButtonImage() -> UIImage? {
let size = bounds.size
UIGraphicsBeginImageContext(CGSize(width: size.width, height: size.height))
defer {
UIGraphicsEndImageContext()
}
guard let ctx = UIGraphicsGetCurrentContext() else {
return nil
}
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
// Want to "over fill" the image area, so the mask can be applied
// to the entire image
let radius = min(size.width / 2.0, size.height / 2.0)
let innerRadius = radius * 0.75
let innerCircle = UIBezierPath(arcCenter: center,
radius: innerRadius,
startAngle: CGFloat(0.0).degreesToRadians,
endAngle: CGFloat(360.0).degreesToRadians,
clockwise: true)
// The color doesn't matter, only it's alpha level
UIColor.red.setStroke()
innerCircle.lineWidth = 4.0
innerCircle.stroke(with: .normal, alpha: 1.0)
let circle = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: CGFloat(0.0).degreesToRadians,
endAngle: CGFloat(360.0).degreesToRadians,
clockwise: true)
UIColor.clear.setFill()
ctx.fill(bounds)
UIColor.white.setFill()
circle.fill(with: .sourceOut, alpha: 1.0)
return UIGraphicsGetImageFromCurrentImageContext()
}
}
nb: This is unoptimised! I would consider caching the result of makeButtonImage and invalidate it when the state/size of the button changes, just beware of that
Why is this approach any "better" then any other? I just want to say, it's not, but what it does create, is a "cut out" of the inner circle
It's a nitpick on my part, but I think it looks WAY better and is a more flexible solution, as you don't "need" a inner circle stroke color, blah, blah, blah
The solution makes use of the CoreGraphics CGBlendModes
Of course I might just do the whole thing in PaintCodeApp and be done with it

Custom circular progress view in awakeFromNib

I have created circle progress view using UIBezierPath and CAShapeLayer with following code.
let center = self.center
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = kCALineCapRound
self.layer.addSublayer(trackLayer)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 10
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
self.layer.addSublayer(shapeLayer)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
If I put that code in my viewDidLoad it works fine. However, when I add UIView from storyboard and set its class to custom class then copy all codes to that custom UIView class and call it in awakeFromNib function it is not working what could be the reason? And how can I use above code with custom UIView class?
Orange view should be my custom view I have added leading, trailing, bottom and fixed height constraint it but it looks like above. Does not circle view need to place center of orange view?
Actually it works but it has wrong position. The position in this case is out of your view so you can't see it. It makes you think that your code doesn't work.
If you want to have circle in the center of your view. Put the code in awakeFromNib and calculate center your self, don't use self.center
override func awakeFromNib() {
let center = CGPoint.init(x: frame.width / 2, y: frame.height / 2)
let trackLayer = CAShapeLayer()
let shapeLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = kCALineCapRound
self.layer.addSublayer(trackLayer)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 10
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
self.layer.addSublayer(shapeLayer)
}
Result

Circular timer using CAShapelayer

Tried to create a circular timer for my app end up with this
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let progressWidth: CGFloat = 10;
let centerX = CGRectGetMidX(rect)
let centerY = CGRectGetMidY(rect)
let center: CGPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect))
let radius: CGFloat = rect.width / 2
var circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
let backgroundCircle = CAShapeLayer()
backgroundCircle.path = circlePath.CGPath
backgroundCircle.fillColor = backgroundCircleFillColor
self.layer.addSublayer(backgroundCircle)
circlePath = UIBezierPath(arcCenter: center, radius: radius-progressWidth/2, startAngle: -CGFloat(GLKMathDegreesToRadians(90)), endAngle:CGFloat(GLKMathDegreesToRadians(currentAngle)), clockwise: true)
let progressCircle = CAShapeLayer()
progressCircle.path = circlePath.CGPath
progressCircle.lineWidth = progressWidth
progressCircle.strokeColor = progressCircleStrokeColor
progressCircle.fillColor = UIColor.clearColor().CGColor
self.layer.addSublayer(progressCircle)
let innerCirclePath = UIBezierPath(arcCenter: center, radius: radius-progressWidth, startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
let innerCircle = CAShapeLayer()
innerCircle.path = innerCirclePath.CGPath
innerCircle.fillColor = innerCircleFillColor
self.layer.addSublayer(innerCircle)
}
List item
Here is the output got from my code:
Main problems faced in this code are
Phone is getting heat while drawing the circle
After drawning half of the circle drowning speed decreased
Please help me with an alternative
Try this :
import UIKit
class CircularProgressBar: UIView {
let shapeLayer = CAShapeLayer()
let secondShapeLayer = CAShapeLayer()
var circularPath: UIBezierPath?
override init(frame: CGRect) {
super.init(frame: frame)
print("Frame: \(self.frame)")
makeCircle()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
makeCircle()
}
func makeCircle(){
let circularPath = UIBezierPath(arcCenter: .zero, radius: self.bounds.width / 2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.orange.cgColor//UIColor.init(red: 0.0/255.0, green: 0.0/255.0, blue: 0.0/255.0, alpha: 1.0).cgColor
shapeLayer.lineWidth = 5.0
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
shapeLayer.position = self.center
shapeLayer.transform = CATransform3DRotate(CATransform3DIdentity, -CGFloat.pi / 2, 0, 0, 1)
self.layer.addSublayer(shapeLayer)
}
func showProgress(percent: Float){
shapeLayer.strokeEnd = CGFloat(percent/100)
}
}
Take a UIView in Storyboard and add it as a subView. Then you can increase the progress using showProgress function.
You shouldn't add layers in drawRect:. Every time your view is drawn, you're adding a layer. That's why it's not surprising that your iPhone is suffering from it and is getting slower and hotter. You should create your layers in viewDidLoad or where your view is created, and you shouldn't modify them in drawRect. This method is only for drawing and nothing else.
I did it like this and worked for me. We need two layers, one for circle and another for progress.
private let circularProgressView = UIView()
private let circleLayer = CAShapeLayer()
private let progressLayer = CAShapeLayer()
private var circularPath: UIBezierPath?
private func setupCircularProgressBar() {
circularPath = UIBezierPath(arcCenter: CGPoint(x: circularProgressView.frame.size.width / 2.0,
y: circularProgressView.frame.size.height / 2.0),
radius: circularProgressView.bounds.width / 2, startAngle: -.pi / 2,
endAngle: 3 * .pi / 2, clockwise: true)
circleLayer.path = circularPath?.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.lineCap = .round
circleLayer.lineWidth = 5
circleLayer.strokeColor = UIColor.darkGray.cgColor
progressLayer.path = circularPath?.cgPath
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineCap = .round
progressLayer.lineWidth = 3
progressLayer.strokeEnd = 0
progressLayer.strokeColor = UIColor.white.cgColor
circularProgressView.layer.addSublayer(circleLayer)
circularProgressView.layer.addSublayer(progressLayer)
}
private func animateCircularProgress(duration: TimeInterval) {
let circularProgressAnimation = CABasicAnimation(keyPath: "strokeEnd")
circularProgressAnimation.duration = duration
circularProgressAnimation.toValue = 1
circularProgressAnimation.fillMode = .forwards
circularProgressAnimation.isRemovedOnCompletion = false
progressLayer.add(circularProgressAnimation, forKey: "progressAnim")
}
usage is very simple you only call animateCircularProgress method with time interval parameter like this with 5 sec duration: animateCircularProgress(duration: 5)

Can't put circular layer that surrounds imageView on different iPhone devices

How can I fix this issue?
The image is set with constraints in the storyboard; the other circle is set with these lines of code:
let centerY = profileImage.center.y+20+(self.navigationController?.navigationBar.frame.size.height)!
let centerX = profileImage.center.x
let center = CGPoint(x: centerX, y: centerY)
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: (profileImage.frame.width/2)+2, startAngle: -CGFloat.pi-CGFloat.pi/2, endAngle: CGFloat.pi/2, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = Functions.hexStringToUIColor(hex: "#3859B9").cgColor
trackLayer.lineWidth = 3
trackLayer.lineCap = kCALineCapRound
trackLayer.fillColor = UIColor.clear.cgColor
view.layer.addSublayer(trackLayer)
On iPhone 7 (the one on the right) seems to be right.
Using fixed values for navigation's or status' bar height will lead to such errors. You should position your layer according to the image view. For that you can simply assign the frame of the image view to your layer:
trackLayer.frame = profileImage.frame
Since the layout may change for several reasons (e.g. device rotation), you should do this in viewDidLayoutSubviews of your view controller.
Since the frame of the layer is now smaller than in your example, you should create the center with:
let center = CGPoint(x: profileImage.bounds.midX, y: profileImage.bounds.midY)
override func viewDidLayoutSubviews() {
let centerY = profileImage.center.y+20+(self.navigationController?.navigationBar.frame.size.height)!
let centerX = profileImage.center.x
let center = CGPoint(x: centerX, y: centerY)
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: (profileImage.frame.width/2)+2, startAngle: -CGFloat.pi-CGFloat.pi/2, endAngle: CGFloat.pi/2, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = Functions.hexStringToUIColor(hex: "#3859B9").cgColor
trackLayer.lineWidth = 3
trackLayer.lineCap = kCALineCapRound
trackLayer.fillColor = UIColor.clear.cgColor
view.layer.addSublayer(trackLayer)
}
OR in write the code in viewDidAppear without delay
This would fix the issue.
Add the layer to profileImage.layer.
trackLayer.frame = profileImage.bounds
Full code should be like this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let center = CGPoint(x: profileImage.bounds.midX,
y: profileImage.bounds.midY)
let trackLayer = CAShapeLayer()
trackLayer.frame = profileImage.frame
let circularPath = UIBezierPath(arcCenter: center,
radius: (profileImage.frame.width / 2) + 2,
startAngle: -CGFloat.pi - CGFloat.pi / 2,
endAngle: CGFloat.pi / 2, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = Functions.hexStringToUIColor(hex: "#3859B9").cgColor
trackLayer.lineWidth = 3
trackLayer.lineCap = kCALineCapRound
trackLayer.fillColor = UIColor.clear.cgColor
profileImage.layer.addSublayer(trackLayer)
}
Using interface builder:
Drag UIView and drop above the imageview
Change view custom class to CirculerProgressView
Set center X and Y constraints with imageview center
Set height and width constraints(greater then imageView height & width)
#IBDesignable class CirculerProgressView: UIView {
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override func draw(_ rect: CGRect) {
super.draw(rect)
setupLayer()
}
private func setupLayer() {
let trackLayer = layer as! CAShapeLayer
let center = CGPoint(x: frame.width/2, y: frame.height/2)
let circularPath = UIBezierPath(arcCenter: center, radius: frame.width/2, startAngle: -CGFloat.pi-CGFloat.pi/2, endAngle: CGFloat.pi/2, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.blue.cgColor
trackLayer.lineWidth = 3
trackLayer.lineCap = kCALineCapRound
trackLayer.fillColor = UIColor.clear.cgColor
}}

Resources