I am looking to have an animated outer layer to a circle that will show progress. What I have below is able to do that; however, the ending animation of the outer fill goes too far. In my example I would like it go from the 12:00 position to the 6:00 position or 50% of the way around the circle. In practice it goes to about the 8:00 position.
class CircleView: UIView {
private let progressLayer = CAShapeLayer()
private var progressColor:UIColor!
required convenience init(progressColor:UIColor) {
self.init(frame:.zero)
self.progressColor = progressColor
}
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func draw(_ rect: CGRect) {
setup()
}
private func setup() {
let circleLayer = CAShapeLayer()
let center = CGPoint(x: self.bounds.size.width / 2, y: self.bounds.size.height / 2)
let path = UIBezierPath(arcCenter: center, radius: self.frame.width / 2, startAngle: -CGFloat.pi / 2, endAngle: CGFloat.pi * 2, clockwise: true)
circleLayer.path = path.cgPath
circleLayer.strokeColor = UIColor.gray.cgColor
circleLayer.fillColor = UIColor.white.cgColor
circleLayer.lineCap = CAShapeLayerLineCap.round
circleLayer.lineWidth = 20
circleLayer.masksToBounds = false
self.layer.addSublayer(circleLayer)
progressLayer.path = path.cgPath
progressLayer.strokeColor = progressColor.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineCap = CAShapeLayerLineCap.round
progressLayer.lineWidth = 20
circleLayer.addSublayer(progressLayer)
}
func animateProgress(percentComplete:Double) {
let progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
progressAnimation.fromValue = 0
progressAnimation.toValue = 0.5
progressAnimation.duration = 2
progressAnimation.fillMode = .forwards
progressAnimation.isRemovedOnCompletion = false
progressLayer.add(progressAnimation, forKey: "strokeEnd")
}
If I change to the above line to progressAnimation.toValue = 0.4 it will show the 12-6 I am looking for. I am not understanding why it would be 0.4 versus 0.5?
It will be OK to fix the angle .
Turn
let path = UIBezierPath(arcCenter: center, radius: self.frame.width / 2, startAngle: -CGFloat.pi / 2, endAngle: CGFloat.pi * 2, clockwise: true)
to
let path = UIBezierPath(arcCenter: center, radius: self.frame.width / 2, startAngle: -CGFloat.pi / 2, endAngle: 1.5 * CGFloat.pi, clockwise: true)
A circle is of 2 * CGFloat.pi, not 2.5 * CGFloat.pi.
startAngle: -CGFloat.pi / 2, endAngle: CGFloat.pi * 2 is 2.5 * CGFloat.pi.
2 * CGFloat.pi * 0.5 equals 2.5 * CGFloat.pi * 0.4
Related
Any suggestion how to implement the following progress chart for iOS Swift?
Just break this down into individual steps.
The first question is how to draw the individual tickmarks.
One way is to draw four strokes using a UIBezierPath:
a clockwise arc at the outer radius;
a line to the inner radius;
a counter-clockwise arc at the inner radius; and
a line back out to the outer radius.
Turns out, you can skip the two lines, and just add those two arcs, and then close the path and you’re done. The UIBezierPath will add the lines between the two arcs for you. E.g.:
let startAngle: CGFloat = 2 * .pi * (CGFloat(i) - 0.2) / CGFloat(tickCount)
let endAngle: CGFloat = 2 * .pi * (CGFloat(i) + 0.2) / CGFloat(tickCount)
// create path for individual tickmark
let path = UIBezierPath()
path.addArc(withCenter: center, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: center, radius: innerRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
// use that path in a `CAShapeLayer`
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = …
shapeLayer.strokeColor = UIColor.clear.cgColor
shapeLayer.path = path.cgPath
// add it to our view’s `layer`
view.layer.addSublayer(shapeLayer)
Repeat this for i between 0 and tickCount, where tickCount is 90, and you have ninety tickmarks:
Obviously, use whatever colors you want, make the ones outside your progress range gray, etc. But hopefully this illustrates the basic idea of how to use UIBezierPath to render two arcs and fill the shape for each respective tick mark with a specified color.
E.g.
class CircularTickView: UIView {
var progress: CGFloat = 0.7 { didSet { setNeedsLayout() } }
private var shapeLayers: [CAShapeLayer] = []
private let startHue: CGFloat = 0.33
private let endHue: CGFloat = 0.66
override func layoutSubviews() {
super.layoutSubviews()
shapeLayers.forEach { $0.removeFromSuperlayer() }
shapeLayers = []
let outerRadius = min(bounds.width, bounds.height) / 2
let innerRadius = outerRadius * 0.7
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let tickCount = 90
for i in 0 ..< tickCount {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = color(percent: CGFloat(i) / CGFloat(tickCount)).cgColor
shapeLayer.strokeColor = UIColor.clear.cgColor
let startAngle: CGFloat = 2 * .pi * (CGFloat(i) - 0.2) / CGFloat(tickCount)
let endAngle: CGFloat = 2 * .pi * (CGFloat(i) + 0.2) / CGFloat(tickCount)
let path = UIBezierPath()
path.addArc(withCenter: center, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: center, radius: innerRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shapeLayer.path = path.cgPath
layer.addSublayer(shapeLayer)
shapeLayers.append(shapeLayer)
}
}
private func color(percent: CGFloat) -> UIColor {
if percent > progress {
return .lightGray
}
let hue = (endHue - startHue) * percent + startHue
return UIColor(hue: hue, saturation: 1, brightness: 1, alpha: 1)
}
}
Clearly, you will want to tweak as you see fit. Perhaps change the color algorithm. Perhaps have it start from 12 o’clock rather than 3 o’clock. Etc. The details are less important than groking the basic idea of how you add shape layers with paths to your UI.
I did the following:
let xOrigin = (pointsOfPerformance[POP.gluteusMaximus.index].x_origin / 375) * self.view.bounds.width
let yOrigin = (pointsOfPerformance[POP.gluteusMaximus.index].y_origin / 666.6666) * (self.view.bounds.width * 16 / 9)
let p0 = CGPoint(x: xOrigin, y: yOrigin)
let xDest = (pointsOfPerformance[POP.upperBack.index].x_origin / 375) * self.view.bounds.width
let yDest = (pointsOfPerformance[POP.upperBack.index].y_origin / 666.6666) * (self.view.bounds.width * 16 / 9)
let p1 = CGPoint(x: xDest, y: yDest)
let midPoint = CGPoint(x: (xOrigin + xDest) / 2, y:
(yOrigin + yDest) / 2)
let path = UIBezierPath()
path.move(to: p0)
path.addLine(to: p1)
path.addArc(withCenter: p2, radius: 94, startAngle: 200, endAngle: 130, clockwise: true)
let centerOfCircle = makePoint(xOffset: p2.x, yOffset: p2.y)
view.addSubview(centerOfCircle)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.animationRed.cgColor
shapeLayer.strokeColor = UIColor.animationRed.cgColor
shapeLayer.lineWidth = 0.05
view.layer.addSublayer(shapeLayer)
How do I get the circle to only show up as a slice of its curve that comes off the line on the back. It's unclear how I manipulate the function to get only the part coming off the line on the back visible.
This works on MacOS:
class CustomView: NSView {
override func draw(_ rect: CGRect) {
let bkgrnd = NSBezierPath(rect: rect)
let fillColor = NSColor.white
fillColor.set()
bkgrnd.fill()
let center = NSMakePoint(rect.size.width/2,rect.size.height/2)
let radius:CGFloat = rect.size.width/4
let path1 = NSBezierPath()
path1.appendArc(withCenter: center, radius: radius, startAngle: 90, endAngle: 270)
NSColor.gray.set()
path1.fill()
let path2 = NSBezierPath()
path2.appendArc(withCenter: center, radius: radius, startAngle: 270, endAngle: 90)
NSColor.blue.set()
path2.fill()
}
}
Demonstrates getting a small slice of the periphery of a circle for MacOS:
class CustomView: NSView {
override func draw(_ rect: CGRect ) {
let bkgrnd = NSBezierPath(rect: rect)
let fillColor = NSColor.white
fillColor.set()
bkgrnd.fill()
let center = NSMakePoint(rect.size.width/2,rect.size.height/2)
let radius:CGFloat = rect.size.width/4
let circle = NSBezierPath()
circle.appendArc(withCenter: center, radius: radius, startAngle: 0, endAngle: 360)
NSColor.gray.set()
circle.fill()
let arc = NSBezierPath()
arc.appendArc(withCenter: center, radius: radius, startAngle: 120, endAngle: 200)
NSColor.blue.set()
arc.fill()
}
}
I want to animate a circle. The circle radius should grow from 0 to 100. I tried it with the transform.scale animation. But of course it is not possible to scale a circle with a radius of 0. When I set the radius of the circle to 1 the circle is visible although it shouldn't be at the beginning of the animation.
minimal example:
let circleShape = CAShapeLayer()
let circlePath = UIBezierPath(arcCenter: .zero, radius: 0, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi*2), clockwise: true)
circleShape.path = circlePath.cgPath
circleShape.fillColor = UIColor.brown.cgColor
let circleAnimation = CABasicAnimation(keyPath: "transform.scale")
circleAnimation.fromValue = 0
circleAnimation.toValue = 100
circleAnimation.duration = 1.9
circleAnimation.beginTime = CACurrentMediaTime() + 1.5
circleShape.add(circleAnimation, forKey: nil)
At the end of the animation the radius should stay at the new value.
EDIT:
Thanks to #Rob mayoff
Final code:
let circleShape = CAShapeLayer()
let circlePath = UIBezierPath(arcCenter: .zero, radius: 400, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi*2), clockwise: true)
circleShape.path = circlePath.cgPath
circleShape.fillColor = UIColor.brown.cgColor
let circleAnimation = CABasicAnimation(keyPath: "transform.scale")
circleAnimation.fromValue = 0.0000001
circleAnimation.toValue = 1
circleAnimation.duration = 1.9
circleAnimation.beginTime = CACurrentMediaTime() + 1.5
circleShape.add(circleAnimation, forKey: nil)
circleAnimation.fillMode = .backwards
Probably what you want to do is set the radius of circlePath to your final radius:
let circlePath = UIBezierPath(arcCenter: .zero, radius: 0, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi*2), clockwise: true)
circlePath.clone()
And then animate from a near-zero number to 1.0:
circleAnimation.fromValue = 0.0001
circleAnimation.toValue = 1.0
If you want to have the animation start in the future (by setting beginTime), then you also probably want to set the fill mode so that the fromValue is applied to the layer until the animation starts:
circleAnimation.fillMode = .backwards
You should use a radius greater 0. Then you can scale it down with circleShape.transform = CATransform3DMakeScale(0.0, 0.0, 0.0)
before adding the layer. And then you could start the animation:
let circleShape = CAShapeLayer()
let center = CGPoint(x: view.frame.width / 2, y: view.frame.height / 2)
let circlePath = UIBezierPath(arcCenter: center, radius: 100.0, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi*2), clockwise: true)
circleShape.transform = CATransform3DMakeScale(0.0, 0.0, 0.0)
circleShape.path = circlePath.cgPath
circleShape.fillColor = UIColor.brown.cgColor
let circleAnimation = CABasicAnimation(keyPath: "transform.scale")
circleAnimation.fromValue = 0
circleAnimation.toValue = 1
circleAnimation.duration = 5.9
circleAnimation.beginTime = CACurrentMediaTime() + 1.5
circleShape.add(circleAnimation, forKey: nil)
view.layer.addSublayer(circleShape)
I'm new to Swift and I don't have too much information about it.I would like to create animated clock with filling 'CAShapeLayer' path in time interval than tried to make it with 'CABasicAnimation' in 60 seconds. The shape fills in 49 sec and animation finish fit in 60 seconds, so it's not working as desired. Than I changed animation.byValue = 0.1 but the result is same. Some one have an idea about this issue?
My code is :
var startAngle = -CGFloat.pi / 2
var endAngle = CGFloat.pi * 2
var shapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
let center = view.center
let circlePath = UIBezierPath(arcCenter: center, radius: 100, startAngle: startAngle, endAngle: endAngle, clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.lineWidth = 20
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
animate(layer: shapeLayer) //Animation func
view.layer.addSublayer(shapeLayer)
}
private func animate(layer: CAShapeLayer) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 60.0 //I think is counting by seconds?
animation.repeatCount = .infinity
animation.fillMode = kCAFillModeForwards
layer.add(animation, forKey: "strokeEndAnimation")
}
The problem is with the way you made the BezierPath.
Try this instead:
let circlePath = UIBezierPath(arcCenter: .zero, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.lineWidth = 20
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = 0
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
shapeLayer.position = self.view.center
shapeLayer.transform = CATransform3DRotate(CATransform3DIdentity, -CGFloat.pi / 2, 0, 0, 1)
The arcCenter must be at .zero and set the shape's center as the center of the view. Your circle will start animating from the right most point though, therefore add a CATransform to rotate the shape 90 degrees counterclockwise.
I want to crop UIView in semi circle shape
Thanks in advance.
A convenient way is just subclass a UIView, add a layer on it and make the view color transparent if it's not by default.
import UIKit
class SemiCirleView: UIView {
var semiCirleLayer: CAShapeLayer!
override func layoutSubviews() {
super.layoutSubviews()
if semiCirleLayer == nil {
let arcCenter = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
let circleRadius = bounds.size.width / 2
let circlePath = UIBezierPath(arcCenter: arcCenter, radius: circleRadius, startAngle: CGFloat.pi, endAngle: CGFloat.pi * 2, clockwise: true)
semiCirleLayer = CAShapeLayer()
semiCirleLayer.path = circlePath.cgPath
semiCirleLayer.fillColor = UIColor.red.cgColor
layer.addSublayer(semiCirleLayer)
// Make the view color transparent
backgroundColor = UIColor.clear
}
}
}
This question was already answered here: Draw a semi-circle button iOS
This is a extract of that question but using a UIView:
Swift 3
let myView = [this should be your view]
let circlePath = UIBezierPath(arcCenter: CGPoint(x: myView.bounds.size.width / 2, y: 0), radius: myView.bounds.size.height, startAngle: 0.0, endAngle: CGFloat(M_PI), clockwise: true)
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
myView.layer.mask = circleShape
Swift 4
let myView = [this should be your view]
let circlePath = UIBezierPath(arcCenter: CGPoint(x: myView.bounds.size.width / 2, y: 0), radius: myView.bounds.size.height, startAngle: 0.0, endAngle: .pi, clockwise: true)
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
myView.layer.mask = circleShape
I hope this helps you
it works in swift 4 and swift 5 for this issue.
let yourView = (is the view you want to crop semi circle from top)
let circlePath = UIBezierPath(arcCenter: CGPoint(x: yourView.bounds.size.width / 2, y: yourView.bounds.size.height), radius: yourView.bounds.size.height, startAngle: 0.0, endAngle: .pi, clockwise: false)
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
yourView.layer.mask = circleShape