animating circular UIBezierPath without rounding the stroke - ios

I'm trying to add an animation to my UIBezierPath, but I want to keep the hard edges my current fillPath is creating, here's a screenshot of what the result should look like:
I did manage to get one animation working correctly using a slightly modified version of this code:
// create an object that represents how the curve
// should be presented on the screen
let progressLine = CAShapeLayer()
progressLine.path = ovalPath.CGPath
progressLine.strokeColor = UIColor.blueColor().CGColor
progressLine.fillColor = UIColor.clearColor().CGColor
progressLine.lineWidth = 10.0
progressLine.lineCap = kCALineCapRound
// add the curve to the screen
self.view.layer.addSublayer(progressLine)
// create a basic animation that animates the value 'strokeEnd'
// from 0.0 to 1.0 over 3.0 seconds
let animateStrokeEnd = CABasicAnimation(keyPath: "strokeEnd")
animateStrokeEnd.duration = 3.0
animateStrokeEnd.fromValue = 0.0
animateStrokeEnd.toValue = 1.0
// add the animation
progressLine.addAnimation(animateStrokeEnd, forKey: "animate stroke end animation")
The above code is from: http://mathewsanders.com/animations-in-swift-part-two/
It did work, but the stroke it created was rounded.
The only examples/tutorials and other questions I could find on this matter, was using CAShapes. I'm currently not using CAShapes or layers. (I don't know if that's needed to make an animation work with this, but I figure I'd ask)
Here's my current code (without any animation). To draw my little circle chart.
class CircleChart: UIView {
var percentFill: Float = 99.00 {
didSet {
// Double check that it's less or equal to 100 %
if percentFill <= maxPercent {
//the view needs to be refreshed
setNeedsDisplay()
}
}
}
var fillColor: UIColor = UIColor.formulaBlueColor()
var chartColor: UIColor = UIColor.formulaLightGrayColor()
override func drawRect(rect: CGRect) {
// Define the center.
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
// Define the radius
let radius: CGFloat = max(bounds.width, bounds.height)
// Define the width of the circle
let arcWidth: CGFloat = 10
// Define starting and ending angles (currently unused)
let startAngle: CGFloat = degreesToRadians(-90)
let endAngle: CGFloat = degreesToRadians(270)
// 5
var path = UIBezierPath(arcCenter: center,
radius: bounds.width/2 - arcWidth/2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
// 6
path.lineWidth = arcWidth
chartColor.setStroke()
path.stroke()
//Draw the fill-part
//calculate the arc for each per percent
let arcLengthPerPercent = degreesToRadians(360/100)
//then multiply out by the actual percent
let fillEndAngle = arcLengthPerPercent * CGFloat(percentFill) + startAngle
//2 - draw the outer arc
var fillPath = UIBezierPath(arcCenter: center,
radius: bounds.width/2 - arcWidth/2,
startAngle: startAngle,
endAngle: fillEndAngle,
clockwise: true)
//3 - draw the inner arc
fillPath.addArcWithCenter(center,
radius: bounds.width/2 - arcWidth/2,
startAngle: fillEndAngle,
endAngle: startAngle,
clockwise: false)
//4 - close the path
fillPath.closePath()
fillColor.setStroke()
fillPath.lineWidth = arcWidth
fillPath.stroke()
}
}
What would be the best way to animate the fillPath? Or do I need to redo everything in CAShapes and layers, to make it work?
Any help would be greatly appreciated.

If you want it square, why are you using progressLine.lineCap = kCALineCapRound? Just delete that line, and you'll get the default butt end (kCALineCapButt).

Related

I want to Rotate 3 different images on different location in 360 degree in swift 4 using UIBezierPath

I want to Rotate 3 different images on different location in 360 degree in swift 4 using UIBezierPath. i am able to move single image like this in image. with this code
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let circlePath = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX, y: view.frame.midY), radius: 120, startAngle: 0, endAngle:CGFloat(Double.pi)*2, clockwise: true)
let animation = CAKeyframeAnimation(keyPath: "position");
animation.duration = 5
animation.repeatCount = MAXFLOAT
animation.path = circlePath.cgPath
let moon = UIImageView()
moon.frame = CGRect(x:0, y:0, width:40, height:40);
moon.image = UIImage(named: "moon.png")
view.addSubview(moon)
moon.layer.add(animation, forKey: nil)
}
}
and I want to rotate 3 different images on different Angle and Position like this.i want to rotate all 3 different images like in this
Anyone here who can update my code according to the image above I want Thank You in Advance Cheers! :) .
I tried with your code and its working. I made a function to rotate a imageView.
func startMovingAView(startAnlge: CGFloat, endAngle: CGFloat) {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX, y: view.frame.midY), radius: 120, startAngle: startAnlge, endAngle:endAngle, clockwise: true)
let animation = CAKeyframeAnimation(keyPath: "position");
animation.duration = 5
animation.repeatCount = MAXFLOAT
animation.path = circlePath.cgPath
let moon = UIImageView()
moon.frame = CGRect(x:0, y:0, width:40, height:40);
moon.image = UIImage(named: "moon.png")
view.addSubview(moon)
moon.layer.add(animation, forKey: nil)
}
And here is the angles what I pass for each imageView:
/// For first imageView
var startAngle: CGFloat = 0.0
var endAngle: CGFloat = CGFloat(Double.pi) * 2.0
startMovingAView(startAnlge: startAngle, endAngle: endAngle)
/// For second imageView
startAngle = CGFloat(Double.pi/2.0)
endAngle = CGFloat(Double.pi) * 2.5
startMovingAView(startAnlge: startAngle, endAngle: endAngle)
/// For third imageView
startAngle = CGFloat(Double.pi)
endAngle = CGFloat(Double.pi) * 3.0
startMovingAView(startAnlge: startAngle, endAngle: endAngle)
We just need to find the angles with same difference so that in 5 seconds duration the imageView have to travel the same distance so all objects moves with constant and same speed.

How are these animations typically done in Swift / iOS - See image

What do I need to learn in order to create similar animations to the ones shown in the following picture?
Can some one list all of the technologies involved and possibly a quick process as to how this is done?
For First animation you can use CAShapeLayer and CABasic Animation and animate key path strokeEnd
I builded exactly same you can have look at this link,download and see option Fill circle animation https://github.com/ajaykumar21091/AwesomeCustomAnimations-iOS/tree/master/SimpleCustomAnimations
Edit -
The basic idea here is to draw circle using bezeir path and animate the shapeLayer using CABasicAnimation using keyPath strokeEnd.
override func drawRect(rect: CGRect) {
self.drawBezierWithStrokeColor(circleColor.CGColor,
startAngle: 0,
endAngle: 360,
animated: false)
self.drawBezierWithStrokeColor(self.fillColor.CGColor,
startAngle: 0,
endAngle: (((2*CGFloat(M_PI))/100) * CGFloat(percentage)),
animated: true)
}
//helper methods.
private func drawBezierWithStrokeColor(color:CGColor, startAngle:CGFloat, endAngle:CGFloat, animated:Bool) {
let bezier:CAShapeLayer = CAShapeLayer()
bezier.path = bezierPathWithStartAngle(startAngle, endAngle: endAngle).CGPath
bezier.strokeColor = color
bezier.fillColor = UIColor.clearColor().CGColor
bezier.lineWidth = bounds.width * 0.18
self.layer.addSublayer(bezier)
if (animated) {
let animatedStrokeEnd:CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd");
animatedStrokeEnd.duration = (2.0/100)*Double(percentage);
animatedStrokeEnd.fromValue = NSNumber(float: 0.0);
animatedStrokeEnd.toValue = NSNumber(float: 1.0);
animatedStrokeEnd.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
bezier.addAnimation(animatedStrokeEnd, forKey: "strokeEndAnimation");
}
}
private func bezierPathWithStartAngle(startAngle:CGFloat, endAngle:CGFloat) -> UIBezierPath {
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let radius = max(bounds.width, bounds.height)
let arcWidth = bounds.width * 0.25
let path = UIBezierPath(arcCenter : center,
radius : radius/2 - arcWidth/2,
startAngle : startAngle,
endAngle : endAngle ,
clockwise : true)
return path
}

After setting variable value, it prints as unchanged

I change the value of a variable with this function:
func scoreDisplay(score:Double) {
print(score)
CounterView().counter = Int(score)
print(CounterView().counter)
}
This changes the value of counter in my CounterView class. That class looks like this:
import UIKit
#IBDesignable class CounterView: UIView {
let possiblePoints = 100
let π:CGFloat = CGFloat(M_PI)
var counter: Int = 20
#IBInspectable var outlineColor: UIColor = UIColor.blueColor()
#IBInspectable var counterColor: UIColor = UIColor.orangeColor()
override func drawRect(rect: CGRect) {
// 1
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
// 2
let radius: CGFloat = max(bounds.width, bounds.height)
// 3
let arcWidth: CGFloat = 76
// 4
let startAngle: CGFloat = 3 * π / 4
let endAngle: CGFloat = π / 4
// 5
var path = UIBezierPath(arcCenter: center,
radius: radius/2 - arcWidth/2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
// 6
path.lineWidth = arcWidth
counterColor.setStroke()
path.stroke()
//Draw the outline
//1 - first calculate the difference between the two angles
//ensuring it is positive
let angleDifference: CGFloat = 2 * π - startAngle + endAngle
//then calculate the arc for each single glass
let arcLengthPerPoint = angleDifference / CGFloat(possiblePoints)
//then multiply out by the actual glasses drunk
let outlineEndAngle = arcLengthPerPoint * CGFloat(counter) + startAngle
//2 - draw the outer arc
var outlinePath = UIBezierPath(arcCenter: center,
radius: bounds.width/2 - 2.5,
startAngle: startAngle,
endAngle: outlineEndAngle,
clockwise: true)
//3 - draw the inner arc
outlinePath.addArcWithCenter(center,
radius: bounds.width/2 - arcWidth + 2.5,
startAngle: outlineEndAngle,
endAngle: startAngle,
clockwise: false)
//4 - close the path
outlinePath.closePath()
outlineColor.setStroke()
outlinePath.lineWidth = 5.0
outlinePath.stroke()
}
}
The score is being passed in properly, and prints the correct value - the problem is that as soon as I change the value of CounterView().counter I try to print it out, but it returns as 20 even though I just set the value to a different number.
It's pretty simple.
You create a new instance of CounterView every time you write
CounterView()
So, with your code
CounterView().counter = Int(score)
print(CounterView().counter)
you have created 2 instances of CounterView. The print function is called on the newly created CounterView so its counter value is the default value you set in the implementation: 20. You need to store the instance in a local variable. For example, your method could look like this
func scoreDisplay(score:Double) {
print(score)
let counterView = CounterView()
counterView.counter = Int(score)
print(counterView.counter)
}
This expression
CounterView().counter
creates a new instance of CounterView each time it's called.
You should create one instance, save it in a variable and access counter from that instance.

Adding curves to the ends of a UIBezierPath(arcCentre)

I have the following code added in the drawRect function of a UIView creating the following attached green arc.
Is there a way I can make the edges at the ends of the curve to look like the red arc rounded edges in the smaller image attached.
[![//Draw the Interior
let center = CGPoint(x: bounds.width/2,
y: bounds.height/2)
//Calculate the radius based on the max dimension of the view.
let radius = max(bounds.width, bounds.height)
//Thickness of the Arc
let arcWidth: CGFloat = 76
//Start and End of Circle angle
let startAngle: CGFloat = 3 * π/4
let endAngle: CGFloat = π/4
let path = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
path.lineWidth = arcWidth
counterColor.setStroke()
path.stroke()]
Set the lineCapStyle property of the bezier path to
.Round:
A line with a rounded end. Quartz draws the line to extend beyond the
endpoint of the path. The line ends with a semicircular arc with a
radius of 1/2 the line’s width, centered on the endpoint.
path.lineCapStyle = .Round

Swift draw several paths in loops

I'm working on changing my previously circlechart, to a donutchart (That can support more than 1 number).
I got all the angle's correct, changed my data to work with an array.
But when I try to draw my paths, it only draws the last one.
Here's an example screenshot of the issue:
In the above example, it's set to draw this array:
cell.circleChart.percentFills = [weight, 10]
where weight is the % shown in the label of the circle. So you can see, that it doesn't draw the first angle, but the starting point for the next angle is correct.
Here's the code where I draw my donut chart.
override func drawRect(rect: CGRect) {
// Define the center.
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
// Define the radius
let radius: CGFloat = max(bounds.width, bounds.height)
// Define starting and ending angles
var startAngle: CGFloat = CGFloat(DegreesToRadians(-90))
let endAngle: CGFloat = CGFloat(DegreesToRadians(270))
// Set up the full path. (for the default background color)
var path = UIBezierPath(arcCenter: center,
radius: bounds.width/2 - arcWidth/2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
// draw the full chart with the background color
path.lineWidth = arcWidth
chartColor.setStroke()
path.stroke()
//calculate the arc for each per percent
let arcLengthPerPercent = CGFloat(DegreesToRadians(360/100))
// Temporary var for loop testing
var i = 0
println("startAngle before loop: \(RadiansToDegrees(Double(startAngle)))")
for percentFill in percentFills {
println("LOOP: \(i)")
if i == 0 {
fillColor = UIColor.formulaBlueColor()
println("BLUE")
} else {
fillColor = UIColor.formulaGreenColor()
println("GREEN")
}
//then multiply out by the actual percent
let fillEndAngle = arcLengthPerPercent * CGFloat(percentFill) + startAngle
println("fillEndAngle: \(RadiansToDegrees(Double(fillEndAngle)))")
//2 - draw the outer arc
var fillPath = UIBezierPath(arcCenter: center,
radius: bounds.width/2 - arcWidth/2,
startAngle: startAngle,
endAngle: fillEndAngle,
clockwise: true)
progressLine.path = fillPath.CGPath
progressLine.strokeColor = fillColor.CGColor
progressLine.fillColor = UIColor.clearColor().CGColor
progressLine.lineWidth = arcWidth
// add the curve to the screen
self.layer.addSublayer(progressLine)
i++
startAngle = startAngle+(arcLengthPerPercent * CGFloat(percentFill))
}
}
I reckon I'm probably going wrong in the last bit, where I add the progressLine as a subLayer, but I don't know what else I should be doing here instead.
Of course I've tested that if I only have 1 value in the array, it draws that as intended (in the blue color)
Any help getting several paths drawn out, would be greatly appreciated!
But when I try to draw my paths, it only draws the last one.
The problem is that progressLine is declared outside this routine. Therefore you are just adding the same path layer over and over as a sublayer - not multiple path sublayers. Therefore it appears only once in your interface, containing the path (segment) that you most recently assigned to it — the last one in the last iteration of the loop.

Resources