How to make stretching a circle effect on ios - ios

I have an UIView with bounding to a circle, something like this
I want to make an effect like stretching it, see the following image
It likes a pullToRefresh effect, there are many libraries or opensources to make pullToRefresh, but they're always belongs to tableview. I just want to make the stretching effect independently. I just have a circled UIView and it's stretched when I pull it
How can I make it

The basic idea is to use bezier paths to outline the precise shape you're looking for. You can then use gesture to change that shape and use display link to animate the returning of the stretched circle back to its circular form:
#IBDesignable
class RefreshView: UIView {
private let shapeLayer = CAShapeLayer()
#IBInspectable
var fillColor: UIColor = UIColor.lightGrayColor() {
didSet {
shapeLayer.fillColor = fillColor.CGColor
}
}
#IBInspectable
var strokeColor: UIColor = UIColor.clearColor() {
didSet {
shapeLayer.strokeColor = strokeColor.CGColor
}
}
#IBInspectable
var lineWidth: CGFloat = 0.0 {
didSet {
shapeLayer.strokeColor = strokeColor.CGColor
}
}
/// Center of main circle is in center top
private var pullDownCenter: CGPoint {
return CGPoint(x: bounds.size.width / 2.0, y: bounds.size.width / 2.0)
}
/// Radius of view spans width of view
private var radius: CGFloat {
return bounds.size.width / 2.0
}
override var frame: CGRect {
get {
return super.frame
}
set {
super.frame = newValue
updatePath()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
configureView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configureView()
}
/// Update the path, add shape layer, and add gesture recognizer
private func configureView() {
shapeLayer.fillColor = fillColor.CGColor
shapeLayer.strokeColor = strokeColor.CGColor
shapeLayer.lineWidth = lineWidth
updatePath()
layer.addSublayer(shapeLayer)
let pan = UIPanGestureRecognizer(target: self, action: #selector(RefreshView.handlePan(_:)))
addGestureRecognizer(pan)
}
/// Update path
private func updatePath() {
shapeLayer.path = stretchyCirclePathWithCenter(pullDownCenter, radius: radius, yOffset: yOffset).CGPath
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
yOffset = yOffsetMax
}
// MARK: Gesture Recognizer
private var yOffset: CGFloat = 0.0 { didSet { updatePath() } }
private var yOffsetMax: CGFloat { return bounds.size.width * 1.5 }
private var yOldOffset: CGFloat = 0.0
func handlePan(gesture: UIPanGestureRecognizer) {
if gesture.state == .Began {
yOldOffset = yOffset
} else if gesture.state == .Changed {
yOffset = yOldOffset + max(0, min(gesture.translationInView(gesture.view).y, yOffsetMax))
} else if gesture.state == .Ended || gesture.state == .Cancelled {
animateBackToCircle()
}
}
// MARK: Animation
private var displayLink: CADisplayLink?
private var duration: CGFloat?
private var startTime: CFAbsoluteTime?
private var originalOffset: CGFloat?
private func animateBackToCircle() {
displayLink = CADisplayLink(target: self, selector: #selector(RefreshView.handleDisplayLink(_:)))
duration = 0.5
originalOffset = yOffset
startTime = CFAbsoluteTimeGetCurrent()
displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
func handleDisplayLink(displayLink: CADisplayLink) {
let percent = CGFloat(CFAbsoluteTimeGetCurrent() - startTime!) / duration!
if percent < 1.0 {
yOffset = originalOffset! * (1.0 - sin(percent * CGFloat(M_PI_2)))
} else {
self.displayLink?.invalidate()
self.displayLink = nil
updatePath()
}
}
// MARK: Stretch circle path
private func stretchyCirclePathWithCenter(center: CGPoint, radius: CGFloat, yOffset: CGFloat = 0.0) -> UIBezierPath {
func pointWithCenter(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
return CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle))
}
if yOffset == 0 {
return UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2.0 * CGFloat(M_PI), clockwise: true)
}
let lowerRadius = radius * (1 - yOffset / yOffsetMax * 0.5)
let yOffsetTop = yOffset / 4
let yOffsetBottom = yOffset / 1.5
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(M_PI), endAngle: 0, clockwise: true)
path.addCurveToPoint(CGPoint(x: center.x + lowerRadius, y:center.y + yOffset), controlPoint1: CGPoint(x: center.x + radius, y:center.y + yOffsetTop), controlPoint2: CGPoint(x: center.x + lowerRadius, y:center.y + yOffset - yOffsetBottom))
path.addArcWithCenter(CGPoint(x: center.x, y:center.y + yOffset), radius: lowerRadius, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: true)
path.addCurveToPoint(CGPoint(x: center.x - radius, y:center.y), controlPoint1: CGPoint(x: center.x - lowerRadius, y:center.y + yOffset - yOffsetBottom), controlPoint2: CGPoint(x: center.x - radius, y:center.y + yOffsetTop))
return path
}
}
This renders something like:
Clearly, feel free to play around with the path as you see fit, add additional flourishes like a spinning arrow or whatever, etc. But this illustrates the basics of constructing bezier path, stretching it with gesture, and animating it back to its circular shape.

There is already a control available for you:
circle with strech
Apart from above i will give a try to this approach also:
I think you can have full streched image, hidden by white view above it. As user swips down or up you can show/hide part of streched image.
if user have not swiped then show unstreched circle image, and if user have swipe a thresold distance show fully streched image.

You will likely need to do custom drawing. Just stretching a view will cause there to be aliasing and the view to be misshaped.
To do this you should subclass UIView and override drawRect to do some kind of drawing that can respond to user interaction.
I've created a sample class:
class CircleRefresh:UIView{
var taperFactor:CGFloat = 0.00{
didSet{
self.setNeedsDisplay()
}
}
var heightFactor: CGFloat = 0.40
{
didSet{
self.setNeedsDisplay()
}
}
var radius:CGFloat
{
return self.frame.size.height * 0.20
}
//Set this to add an arrow or another image to the top circle
var refreshImage:UIImage?{
didSet{
//
self.subviews.forEach{$0.removeFromSuperview()}
if let image = self.refreshImage{
let imageView = UIImageView(frame: CGRect(x:self.frame.size.width * 0.5 - self.radius,y:0.0,width:self.radius * 2.0,height:self.radius * 2.0))
imageView.image = image
imageView.contentMode = .ScaleAspectFit
imageView.layer.cornerRadius = imageView.frame.size.height * 0.5
imageView.layer.masksToBounds = true
self.addSubview(imageView)
}
}
}
override func drawRect(rect:CGRect)
{
UIColor.lightGrayColor().setFill()
let endCenter = CGPoint(x:self.frame.size.width * 0.5,y:self.frame.size.height * heightFactor - radius * taperFactor)
//top arc
let path = UIBezierPath(arcCenter: CGPoint(x:self.frame.size.width * 0.5, y:radius), radius: self.frame.size.height * 0.20, startAngle: 0.0, endAngle: CGFloat(M_PI), clockwise: false)
//left curve
path.addCurveToPoint(CGPoint(x:endCenter.x - radius * taperFactor,y:endCenter.y), controlPoint1: CGPoint(x:endCenter.x - radius, y:radius * 2.0), controlPoint2: CGPoint(x: endCenter.x - radius * taperFactor, y: radius * 2.0))
//bottom arc
path.addArcWithCenter(endCenter, radius: radius * taperFactor, startAngle: CGFloat(M_PI), endAngle: 0.0, clockwise: false)
//right curve
path.addCurveToPoint(CGPoint(x:endCenter.x + radius,y:radius), controlPoint1: CGPoint(x: endCenter.x + radius * taperFactor, y: radius * 2.0),controlPoint2: CGPoint(x:endCenter.x + radius, y:radius * 2.0))
path.fill()
}
}
This will draw a circle but changing taperFactor and heightFactor will give a stretching effect. Setting heightFactor to 1.0 will mean the drawing will take up the entire height of the view and setting taperFactor to 1.0 will cause the tail of the drawing to be as wide as the top circle.
To get a sense for how changing heightFactor and taperFactor changes the drawing, you can then add a pan gesture recognizer to this view and in it's action say something like:
#IBAction func pan(sender: UIPanGestureRecognizer) {
let location = sender.locationInView(self.view)
circle.heightFactor = location.y / self.view.frame.size.height
circle.taperFactor = circle.heightFactor * 0.5 - 0.20
}
You can change heightFactor and taperFactor to achieve different effects. This is only part of how to make a refresh controller but seems to be the thrust of your question.

Related

Chart with UIBezierPath at one below one

I am trying to make pie chart as below which is working fine but I have issue with end path (which is orange in below image).
What I want to do is make end of orange shape to below the green one so that I can achieve as below.
Any suggestion how this can be done?
Code can be found at below link.
https://drive.google.com/file/d/1ST0zNooLgRaI8s2pDK3NMjBQYjBSRoXB/view?usp=sharing
Below is what I have.
func drawBeizer(start_angle : CGFloat, end_angle : CGFloat, final_color : UIColor) {
let path1 : UIBezierPath = UIBezierPath()
path1.addArc(withCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: ((self.frame.size.width-main_view_width)/2), startAngle: start_angle, endAngle: end_angle, clockwise: true)
path1.lineWidth = main_view_width
path1.lineCapStyle = .round
final_color.setStroke()
path1.stroke()
}
This function I am passing start angle and end angle & color for the path.
This solution is for SwiftUI using Path.
struct DonutElement: Shape{
var width: CGFloat = 50
var startAngle: Angle
var endAngle: Angle
func path(in rect: CGRect) -> Path {
// define commonly used vars
// the extend of the shape
let outerRadius = min(rect.width, rect.height) / 2
// the middle of the shape
let midRadius = outerRadius - width / 2
// the inner radius
let innerRadius = outerRadius - width
// centerpoint used to move coordinate system in to center
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
return Path{ path in
// calculate the startpoint from the startAngle. startAngle is allready normalized
let startPoint = startAngle.polarCoordinates(center: center, radius: outerRadius)
// move the path without drawing to the starting position
path.move(to: startPoint)
// add the starting concav arc
// the center point for this arc are the polar coordinates relative to midradius of the donut
path.addArc(center: startAngle.polarCoordinates(center: center, radius: midRadius), radius: width / 2, startAngle: startAngle.normalizing(), endAngle: (.init(degrees: 180) - startAngle), clockwise: true)
// add the arc that presents the inner donut line
// center point is the center of the drawing rect with normalized angles
path.addArc(center: center, radius: innerRadius, startAngle: startAngle.normalizing(), endAngle: endAngle.normalizing(), clockwise: true)
// add the convex arc at the end of the donut element
// switch clockwise to false and swap end and start angle
// replace startAngle with endAngle
path.addArc(center: endAngle.polarCoordinates(center: center, radius: midRadius), radius: width / 2, startAngle: (.init(degrees: 180) - endAngle), endAngle: endAngle.normalizing(), clockwise: false)
// add the outer stroke to close the shape
path.addArc(center: center, radius: outerRadius, startAngle: endAngle.normalizing(), endAngle: startAngle.normalizing(), clockwise: false)
// just in case
path.closeSubpath()
}
}
}
extension Shape {
func fillWithStroke<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
self
.stroke(strokeStyle, lineWidth: lineWidth)
.background(self.fill(fillStyle))
}
}
struct AngleContainer: Identifiable, Hashable{
let id = UUID()
var startAngle: Angle
var endAngle: Angle
var color: Color
}
struct Donut: View{
// each element ends here and starts at the previous or 0. Values in percent
var elementStops: [(CGFloat , Color)] = [(0.4 , .blue), (45 , .red), (55 , .green), (78 , .gray), (100 , .white)]
var width: CGFloat = 50.0
private var angles: [AngleContainer] {
var angles = [AngleContainer]()
for (index, stop) in elementStops.enumerated(){
if index == 0{
let startAngle = Angle(degrees: 0)
let endAngle = Angle(degrees: stop.0/100 * 360)
angles.append(AngleContainer(startAngle: startAngle,endAngle: endAngle,color: stop.1))
} else{
let startAngle = Angle(degrees: elementStops[index - 1].0 / 100 * 360)
let endAngle = Angle(degrees: stop.0/100 * 360)
angles.append(AngleContainer(startAngle: startAngle,endAngle: endAngle,color: stop.1))
}
}
return angles
}
var body: some View{
ZStack{
ForEach(angles){ angleContainer in
DonutElement(width: width, startAngle: angleContainer.startAngle, endAngle: angleContainer.endAngle)
.fillWithStroke(angleContainer.color, strokeBorder: .black.opacity(0.2))
}
}
.background(.white)
}
}
// used for transfering coordinate system
extension CGPoint{
mutating func transfered(to center: CGPoint){
x = center.x + x
y = center.y - y
}
func transfering(to center: CGPoint) -> CGPoint{
.init(x: center.x + x, y: center.y - y)
}
}
I am normalizing angles to behave more like in math with 90 degrees at the top and not at the bottom.
// convenience so angle feels more natural
extension Angle{
func normalizing() -> Angle {
Angle(degrees: 0) - self
}
func polarCoordinates(center: CGPoint, radius: CGFloat) -> CGPoint{
CGPoint(x: cos(self.radians) * radius, y: sin(self.radians) * radius )
.transfering(to: center)
}
}
Result:
I found a way to do this. Below is the full code.
I am adding other addArc using end angles.
ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myView: MyView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
myView.setupData(inner_color: [.green, .purple, .blue, .orange], inner_view_width: self.view.frame.width/5.0, inner_angle: [0.0, 90.0, 180.0, 270.0])
myView.backgroundColor = UIColor.white
}
}
MyView.swift
import UIKit
class MyView: UIView {
var main_radius : CGFloat = 0.0
var main_color : [UIColor] = [UIColor.black, UIColor.green]
var main_view_width : CGFloat = 1.0
var main_angle : [CGFloat] = [-CGFloat.pi, 0.0]
var actual_angle : [CGFloat] = []
func setupData(inner_color : [UIColor], inner_view_width : CGFloat, inner_angle : [CGFloat]) {
main_color = inner_color
main_view_width = inner_view_width
main_angle = inner_angle
actual_angle = inner_angle
var temp_main_angle : [CGFloat] = []
for i in 0..<main_angle.count {
temp_main_angle.append(main_angle[i]*Double.pi/180)
}
main_angle = temp_main_angle
draw(CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.width))
}
override func draw(_ rect: CGRect) {
if (main_color.count >= 2) {
var inner_position : Int = 0
// first draw with sharp corner
for i in 0..<main_color.count {
if (i == (main_color.count-1)) {
inner_position = 0
} else {
inner_position = i+1
}
drawBeizer(start_angle: main_angle[i], end_angle: main_angle[inner_position], final_color: main_color[i])
}
// append with ending icons for circle
for i in 0..<main_color.count {
if (i == (main_color.count-1)) {
inner_position = 0
} else {
inner_position = i+1
}
drawBeizer(start_angle: main_angle[inner_position], end_angle: main_angle[inner_position]+(1*Double.pi/180), final_color: main_color[i], withCircle: true) // make here final_color as .black to see what I am doing with this loop
}
}
}
func drawBeizer(start_angle : CGFloat, end_angle : CGFloat, final_color : UIColor, withCircle: Bool = false) {
let path1 : UIBezierPath = UIBezierPath()
path1.addArc(withCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: ((self.frame.size.width-main_view_width)/2), startAngle: start_angle, endAngle: end_angle, clockwise: true)
path1.lineWidth = main_view_width
if (withCircle) {
path1.lineCapStyle = .round
}
final_color.setStroke()
path1.stroke()
}
}
Full code here

Ball path not following semi-circle exactly

Bit stumped here.
First of all, here is what's going on:
As you can see, my ball is not following the curved quarter-circle path exactly, but vaguely.
Here is the code creating the quarter-circle (p.s. - my container view is 294 units tall and wide):
let startAngle = CGFloat(Double.pi * 2) // top of circle
let endAngle = startAngle + 2 * Double.pi * 0.25
view.layoutIfNeeded()
smallCircleView.parentVC = self
smallCircleView.layer.cornerRadius = 45/2
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: containerView.frame.self.width, startAngle: startAngle, endAngle: endAngle, clockwise: true)
And here is the code shifting the ball around:
func shiftSmallCircleView(newX : CGFloat){
smallCircleViewLeadingConstraint.constant = newX
let angle = (newX/containerView.frame.self.width)*90 + 180
let y = containerView.frame.size.width * cos((Double.pi * 2 * angle) / 360)
smallCircleViewBottomConstraint.constant = y + containerView.frame.origin.y
}
Since I'm using the cos function, should the ball's path be identical to the original quarter-circle path? How can they be similar but not identical?
Edit:
New outcome with updated code:
let angle = (distanceDelta/containerView.frame.self.width) * -90.0
containerView.transform = CGAffineTransform.init(rotationAngle: angle * Double.pi/180)
Most recent edit:
let angle = (distanceDelta/pathContainerView.frame.self.width) * .pi / -180.0
containerView.transform = CGAffineTransform.init(rotationAngle: angle)
All code:
class SmallCircleView : UIView {
var parentVC : ViewController!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as? UITouch {
let point = touch.location(in: self)
}
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as? UITouch {
let point = touch.location(in: self.superview)=
parentVC.shiftSmallCircleView(distanceDelta: point.x)=
}
}
}
class ViewController: UIViewController {
#IBOutlet var containerView : UIView!
#IBOutlet var pathContainerView : UIView!
#IBOutlet var smallCircleView : SmallCircleView!
#IBOutlet var smallCircleViewLeadingConstraint : NSLayoutConstraint!
#IBOutlet var smallCircleViewBottomConstraint : NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let startAngle = CGFloat(Double.pi * 2) // top of circle
let endAngle = startAngle + 2 * Double.pi * 0.25
view.layoutIfNeeded()
smallCircleView.parentVC = self
smallCircleView.layer.cornerRadius = 45/2
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: pathContainerView.frame.self.width, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let shapeLayer = CAShapeLayer()
// The Bezier path that we made needs to be converted to
// a CGPath before it can be used on a layer.
shapeLayer.path = circlePath.cgPath
// apply other properties related to the path
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.position = CGPoint(x: 0, y: 0)
// add the new layer to our custom view
pathContainerView.layer.addSublayer(shapeLayer)
containerView.bringSubviewToFront(smallCircleView)
}
func shiftSmallCircleView(distanceDelta : CGFloat){
let degrees = min(1, (distanceDelta/pathContainerView.frame.size.width)) * -90
containerView.transform = CGAffineTransform.init(rotationAngle: degrees * M_PI/180)
}
}
I’m on my phone right now and so I can’t provide the code but there is a much much easier way to do this. Don’t bother trying to work out what coordinates the ball needs to be at. Just place the ball into a rectangular view with the centre of this view being at the centre of your circle and the ball being on the path. (Make the container view invisible).
Now… rotate the container view.
That’s it.
Because the ball is a child of the view it will be moved as part of the rotation. And the movement will follow a circle centred around the point of rotation. Which is the centre of the container view.
Example
I made a quick example to show what I mean. In essence... cheat. Don't actually do the hard maths to work out where the ball will be. Use methods to make it look the same in an easier way...
Here is my storyboard...
And the code...
class ViewController: UIViewController {
#IBOutlet weak var containerView: UIView!
#IBOutlet weak var circleView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
circleView.layer.cornerRadius = 20
}
#IBAction func sliderChanged(_ sender: UISlider) {
let rotationAngle = sender.value * .pi / 180
containerView.transform = CGAffineTransform(rotationAngle: CGFloat(rotationAngle))
}
}
And an animation...
And if you make the container background clear...
To answer your original question...
I haven't double-checked your math, but this is another method of positioning your "small circle" view.
Using these two "helper" extensions:
extension CGPoint {
static func pointOnCircle(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
}
extension CGFloat {
var degreesToRadians: Self { self * .pi / 180 }
var radiansToDegrees: Self { self * 180 / .pi }
}
We can find the point on the arc for a given angle like this:
let arcCenter: CGPoint = .zero
let radius: CGFloat = 250
let degree: CGFloat = 45
let p = CGPoint.pointOnCircle(center: arcCenter, radius: radius, angle: degree.degreesToRadians)
We can then move the "ball" to that point:
circleView.center = p
To get the circle view to "roll along the inside" of the arc, we use the same center point, but decrease the radius of the arc by the radius of the circle (half the width of the view).
If you want to use that approach (rather than rotating a view with the circle in the corner), here is some example code.
Start with our extensions, an enum, and a "small circle view":
extension CGPoint {
static func pointOnCircle(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
}
extension CGFloat {
var degreesToRadians: Self { self * .pi / 180 }
var radiansToDegrees: Self { self * 180 / .pi }
}
enum FollowType: Int {
case inside, center, outside
}
class SmallCircleView : UIView {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.height / 2.0
}
}
Next, a UIView subclass that will handle drawing the arc, adding the circle subview, and it will use a UIViewPropertyAnimator with key frames to make it interactive:
class FollowArcView: UIView {
public var circleColor: UIColor = .red {
didSet {
circleView.backgroundColor = circleColor
}
}
public var arcColor: UIColor = .blue { didSet { setNeedsLayout() } }
public var arcLineWidth: CGFloat = 1 { didSet { setNeedsLayout() } }
public var arcInset: CGFloat = 0 { didSet { setNeedsLayout() } }
public var circleRadius: CGFloat = 25 { didSet { setNeedsLayout() } }
public var followType: FollowType = .inside { didSet { setNeedsLayout() } }
public var fractionComplete: CGFloat = 0 {
didSet {
if animator != nil {
animator.fractionComplete = fractionComplete
}
}
}
private let circleView = SmallCircleView()
private let arcLayer = CAShapeLayer()
private var animator: UIViewPropertyAnimator!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
arcLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(arcLayer)
circleView.frame = CGRect(x: 0, y: 0, width: circleRadius * 2.0, height: circleRadius * 2.0)
circleView.backgroundColor = circleColor
addSubview(circleView)
}
override func layoutSubviews() {
super.layoutSubviews()
var curFraction: CGFloat = 0
// we may be changing properties (such as arc color, inset, etc)
// after we've moved the circleView, so
// save the current .fractionComplete
if animator != nil {
curFraction = animator.fractionComplete
animator.stopAnimation(true)
}
// these properties can be changed after initial view setup
arcLayer.lineWidth = arcLineWidth
arcLayer.strokeColor = arcColor.cgColor
circleView.frame.size = CGSize(width: circleRadius * 2.0, height: circleRadius * 2.0)
let arcCenter = CGPoint(x: arcInset, y: arcInset)
let arcRadius = bounds.width - (arcInset * 2.0)
let followRadius = followType == .inside ? arcRadius - circleRadius : followType == .center ? arcRadius : arcRadius + circleRadius
let pth = UIBezierPath(arcCenter: arcCenter, radius: arcRadius, startAngle: .pi * 0.5, endAngle: 0, clockwise: false)
arcLayer.path = pth.cgPath
let p = CGPoint.pointOnCircle(center: arcCenter, radius: followRadius, angle: CGFloat(90).degreesToRadians)
circleView.center = p
// the animator will take the current position of the circleView
// as Frame 0, so we need to let UIKit update the circleView's position
// before setting up the animator
DispatchQueue.main.async {
self.setupAnim()
self.animator.fractionComplete = curFraction
}
}
private func setupAnim() {
if animator != nil {
animator.stopAnimation(true)
}
// starting point
var startDegrees: CGFloat = 90
// ending point
let endDegrees: CGFloat = 0
// we'll be using percentages
let numSteps: CGFloat = 100
let arcCenter = CGPoint(x: arcInset, y: arcInset)
let arcRadius = bounds.width - (arcInset * 2.0)
let followRadius = followType == .inside ? arcRadius - circleRadius : followType == .center ? arcRadius : arcRadius + circleRadius
animator = UIViewPropertyAnimator(duration: 0.3, curve: .linear)
animator.addAnimations {
UIView.animateKeyframes(withDuration: 0.1, delay: 0.0, animations: {
let stepDegrees: Double = (startDegrees - endDegrees) / Double(numSteps)
for i in 1...Int(numSteps) {
// decrement degrees by step value
startDegrees -= stepDegrees
// get point on discPathRadius circle
let p = CGPoint.pointOnCircle(center: arcCenter, radius: followRadius, angle: startDegrees.degreesToRadians)
// duration is 1 divided by number of steps
let duration = 1.0 / Double(numSteps)
// start time for this frame is duration * this step
let startTime = duration * Double(i)
// add the keyframe
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: duration) {
self.circleView.center = p
}
}
})
}
// start and immediately pause the animation
animator.startAnimation()
animator.pauseAnimation()
}
}
and an example controller class:
class FollowArcVC: UIViewController {
let followArcView = FollowArcView()
override func viewDidLoad() {
super.viewDidLoad()
let slider = UISlider()
let typeControl = UISegmentedControl(items: ["Inside", "Center", "Outside"])
[followArcView, typeControl, slider].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
followArcView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
followArcView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
followArcView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// 1:1 ratio (square)
followArcView.heightAnchor.constraint(equalTo: followArcView.widthAnchor),
typeControl.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
typeControl.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
typeControl.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
slider.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
typeControl.selectedSegmentIndex = 0
typeControl.addTarget(self, action: #selector(typeChanged(_:)), for: .valueChanged)
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
// if we want to see the view frame
//followArcView.backgroundColor = .yellow
}
#objc func typeChanged(_ sender: UISegmentedControl) -> Void {
switch sender.selectedSegmentIndex {
case 0:
followArcView.followType = .inside
case 1:
followArcView.followType = .center
case 2:
followArcView.followType = .outside
default:
()
}
}
#objc func sliderChanged(_ sender: Any?) {
guard let sldr = sender as? UISlider else { return }
followArcView.fractionComplete = CGFloat(sldr.value)
}
}
The result:
Edit
After playing around a bit, this is another way to interactively follow the path -- it uses layer path animation, and avoids the need to manually calculate keyframe positions.
Works with the same sample view controller as above - just replace the FollowArcView class:
class FollowArcView: UIView {
public var circleColor: UIColor = .red {
didSet {
circleView.backgroundColor = circleColor
}
}
public var arcColor: UIColor = .blue {
didSet {
arcLayer.strokeColor = arcColor.cgColor
}
}
public var arcLineWidth: CGFloat = 1 {
didSet {
arcLayer.lineWidth = arcLineWidth
}
}
public var arcInset: CGFloat = 0 { didSet { setNeedsLayout() } }
public var circleRadius: CGFloat = 25 { didSet { setNeedsLayout() } }
public var followType: FollowType = .inside { didSet { setNeedsLayout() } }
public var fractionComplete: CGFloat = 0 {
didSet {
circleView.layer.timeOffset = CFTimeInterval(fractionComplete)
}
}
private let circleView = SmallCircleView()
private let arcLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
arcLayer.fillColor = UIColor.clear.cgColor
arcLayer.lineWidth = arcLineWidth
arcLayer.strokeColor = arcColor.cgColor
layer.addSublayer(arcLayer)
circleView.frame = CGRect(x: 0, y: 0, width: circleRadius * 2.0, height: circleRadius * 2.0)
circleView.backgroundColor = circleColor
addSubview(circleView)
}
override func layoutSubviews() {
super.layoutSubviews()
circleView.frame.size = CGSize(width: circleRadius * 2.0, height: circleRadius * 2.0)
let arcCenter = CGPoint(x: arcInset, y: arcInset)
let arcRadius = bounds.width - (arcInset * 2.0)
let pth = UIBezierPath(arcCenter: arcCenter, radius: arcRadius, startAngle: .pi * 0.5, endAngle: 0, clockwise: false)
arcLayer.path = pth.cgPath
self.setupAnim()
}
private func setupAnim() {
let arcCenter = CGPoint(x: arcInset, y: arcInset)
let arcRadius = bounds.width - (arcInset * 2.0)
let followRadius = followType == .inside ? arcRadius - circleRadius : followType == .center ? arcRadius : arcRadius + circleRadius
let pth = UIBezierPath(arcCenter: arcCenter, radius: followRadius, startAngle: .pi * 0.5, endAngle: 0, clockwise: false)
let animation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
animation.duration = 1
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
animation.path = pth.cgPath
circleView.layer.speed = 0
circleView.layer.timeOffset = 0
circleView.layer.add(animation, forKey: "PathAnim")
DispatchQueue.main.async {
self.circleView.layer.timeOffset = self.fractionComplete
}
}
}

Drawing a scale on semi-circle Swift/UIView

Imagine I am having a full semi-circle from 0 to Pi from the unit circle. There is a small number on the left side named min and a big number on the right side called max. There are both interchangeable inside the app depending on some factors.
Does anybody of you have a nice idea on how to draw a scale like I did in the drawing below? I would like to have longer lines for every x mod 10 = 0 and three larger ones in between. The grey circle is just for orientation.
So I started with the following piece of code:
let radius = CGFloat(40)
let dashLong = CGFloat(10)
let dashShort = CGFloat(5)
let middle = CGPoint(x: 50, y: 50)
let leftAngle = CGFloat(Double.pi)
let rightAngle = CGFloat(0)
let min = 45 //random num
let max = 117 //random num
let innerPath = UIBezierPath(arcCenter: middle, radius: radius, startAngle: rightAngle, endAngle: leftAngle, clockwise: true)
let middlePath = UIBezierPath(arcCenter: middle, radius: radius+dashShort, startAngle: rightAngle, endAngle: leftAngle, clockwise: true)
let outerPath = UIBezierPath(arcCenter: middle, radius: radius+dashLong, startAngle: rightAngle, endAngle: leftAngle, clockwise: true)
So there is a radius and also the length of the two types of dashes in the scale. I chose 45 and 117 as random integers for the extrem values of the scale. My three paths which do not need to be drawn are just an orientation on where the dashes need to be started and ended on. So for 50,60,...110 there start at the innerPath and go to the outer one, I am pretty sure that must be in the same angle for a dash on all circles.
Does anyone has a very smart idea how to continue this to calc the dashes and draw them without getting messed up code?
Here's the math for drawing a tick mark.
Let's do everything as CGFloat to keep the conversions to a minimum:
let radius: CGFloat = 40
let dashLong: CGFloat = 10
let dashShort: CGFloat 5
let middle = CGPoint(x: 50, y: 50)
let leftAngle: CGFloat = .pi
let rightAngle: CGFloat = 0
let min: CGFloat = 45 //random num
let max: CGFloat = 117 //random num
First, compute your angle.
let value: CGFloat = 50
let angle = (max - value)/(max - min) * .pi
Now compute your two points:
let p1 = CGPoint(x: middle.x + cos(angle) * radius,
y: middle.y - sin(angle) * radius)
// use dashLong for a long tick, and dashShort for a short tick
let radius2 = radius + dashLong
let p2 = CGPoint(x: middle.x + cos(angle) * radius2,
y: middle.y - sin(angle) * radius2)
Then draw a line between p1 and p2.
Note: In iOS, the coordinate system is upside down with +Y being down, which is why the sin calculation is subtracted from middle.y.
Complete Example
enum TickStyle {
case short
case long
}
class ScaleView: UIView {
// ScaleView properties. If any are changed, redraw the view
var radius: CGFloat = 40 { didSet { self.setNeedsDisplay() } }
var dashLong: CGFloat = 10 { didSet { self.setNeedsDisplay() } }
var dashShort: CGFloat = 5 { didSet { self.setNeedsDisplay() } }
var middle = CGPoint(x: 50, y: 50) { didSet { self.setNeedsDisplay() } }
var leftAngle: CGFloat = .pi { didSet { self.setNeedsDisplay() } }
var rightAngle: CGFloat = 0 { didSet { self.setNeedsDisplay() } }
var min: CGFloat = 45 { didSet { self.setNeedsDisplay() } }
var max: CGFloat = 117 { didSet { self.setNeedsDisplay() } }
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
// draw the arc
path.move(to: CGPoint(x: middle.x - radius, y: middle.y))
path.addArc(withCenter: middle, radius: radius, startAngle: leftAngle, endAngle: rightAngle, clockwise: true)
let startTick = ceil(min / 2.5) * 2.5
let endTick = floor(max / 2.5) * 2.5
// add tick marks every 2.5 units
for value in stride(from: startTick, through: endTick, by: 2.5) {
let style: TickStyle = value.truncatingRemainder(dividingBy: 10) == 0 ? .long : .short
addTick(at: value, style: style, to: path)
}
// stroke the path
UIColor.black.setStroke()
path.stroke()
}
// add a tick mark at value with style to path
func addTick(at value: CGFloat, style: TickStyle, to path: UIBezierPath) {
let angle = (max - value)/(max - min) * .pi
let p1 = CGPoint(x: middle.x + cos(angle) * radius,
y: middle.y - sin(angle) * radius)
var radius2 = radius
if style == .short {
radius2 += dashShort
} else if style == .long {
radius2 += dashLong
}
let p2 = CGPoint(x: middle.x + cos(angle) * radius2,
y: middle.y - sin(angle) * radius2)
path.move(to: p1)
path.addLine(to: p2)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let view = ScaleView(frame: CGRect(x: 50, y: 50, width: 100, height: 60))
view.backgroundColor = .yellow
self.view.addSubview(view)
}
}
Picture of scale running in app:
My suggestion is Draw this semi circle in CALayer and Draw lines from centre of the semi circle in different CALayer and Mask both of them so that It appears Like this

iOS Swift - Animate drawing of arc filled, not the border stroke

I am trying to Implement drawing of Arc from certain start angle to certain end angle. I am trying to animate drawing of arc but have no luck.
I have looked for couple of implementations but all of them refers to be drawing of animating boder of circle.
Like drawing a pie but with one color. With certain start and end angle. Animated drawing of pie.
What I like is to have have animated filling while arc is being animated from start angle to end angle.
Like arc is being drawn clockwise and while drawing it is filling its inside color.
Any idea How can I acheive this in Swift?
Thanks in advance.
Edit
This is what I have drawn:
(gray color shows area covered by drawing pie)
Code for drawing Pie
import Foundation
import UIKit
#IBDesignable class PieChart : UIView {
#IBInspectable var percentage: CGFloat = 50 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var outerBorderWidth: CGFloat = 2 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var outerBorderColor: UIColor = UIColor.white {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var percentageFilledColor: UIColor = UIColor.primary {
didSet {
self.setNeedsDisplay()
}
}
let circleLayer = CAShapeLayer()
override func draw(_ rect: CGRect) {
drawPie(rect: rect, endPercent: percentage, color: UIColor.primary)
}
func drawPie(rect: CGRect, endPercent: CGFloat = 70, color: UIColor) {
let center = CGPoint(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2)
let radius = min(rect.width, rect.height) / 2
let gpath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat(360.degreesToRadians), clockwise: true)
UIColor.white.setFill()
gpath.fill()
let circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0), endAngle:CGFloat(360.degreesToRadians), clockwise: true)
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = outerBorderColor.cgColor
circleLayer.lineWidth = outerBorderWidth
circleLayer.borderColor = outerBorderColor.cgColor
self.layer.addSublayer(circleLayer)
let π: CGFloat = 3.14
let halfPi: CGFloat = π / 2
let startPercent: CGFloat = 0.0
let startAngle = (startPercent / 100 * π * 2 - π ) + halfPi
let endAngle = (endPercent / 100 * π * 2 - π ) + halfPi
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true)
path.close()
percentageFilledColor.setFill()
path.fill()
}
}
I've created this code using playground and swift. This code will draw a pie chart animating the filling color. Make sure you have the Timeline pane open on Xcode to see the animation working. I couldn't find a way to pass the path function directly to CAAnimation so my workaround was to create a list of steps, each one containing the path for a percentage stage. You can change the steps for a smother animation.
//: Playground - noun: a place where people can play
import UIKit
import XCPlayground
let STEPS_ANIMATION = 50
let initialPercentage : CGFloat = 0.10
let finalPercentage : CGFloat = 0.75
func buildPiePath(frame : CGRect, percentage : CGFloat) -> UIBezierPath {
let newPath = UIBezierPath()
let startPoint = CGPoint(x: frame.size.width, y: frame.height / 2.0)
let centerPoint = CGPoint(x: frame.size.width / 2.0, y: frame.height / 2.0)
let startAngle : CGFloat = 0
let endAngle : CGFloat = percentage * 2 * CGFloat(M_PI)
newPath.move(to: centerPoint)
newPath.addLine(to: startPoint)
newPath.addArc(withCenter: centerPoint,
radius: frame.size.width / 2.0,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
newPath.addLine(to: centerPoint)
newPath.close()
return newPath
}
func buildPiePathList(frame: CGRect,
startPercentage: CGFloat,
finalPercentage: CGFloat) -> [AnyObject] {
var listValues = [AnyObject]()
for index in 1...STEPS_ANIMATION {
let delta = finalPercentage - startPercentage
let currentPercentage = CGFloat(index) / CGFloat(STEPS_ANIMATION)
let percentage = CGFloat(startPercentage + (delta * currentPercentage))
listValues.append(buildPiePath(frame: frame,
percentage: percentage)
.cgPath)
}
return listValues
}
// Container for pie chart
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
container.backgroundColor = UIColor.gray
XCPShowView(identifier: "Container View", view: container)
let circleFrame = CGRect(x: 20, y: 20, width: 360, height: 360)
// Red background
let background = CAShapeLayer()
background.frame = circleFrame
background.fillColor = UIColor.red.cgColor
background.path = buildPiePath(frame: circleFrame, percentage: 1.0).cgPath
container.layer.addSublayer(background)
// Green foreground that animates
let foreground = CAShapeLayer()
foreground.frame = circleFrame
foreground.fillColor = UIColor.green.cgColor
foreground.path = buildPiePath(frame: circleFrame, percentage: initialPercentage).cgPath
container.layer.addSublayer(foreground)
// Filling animation
let fillAnimation = CAKeyframeAnimation(keyPath: "path")
fillAnimation.values = buildPiePathList(frame: circleFrame,
startPercentage: initialPercentage,
finalPercentage: finalPercentage)
fillAnimation.duration = 3
fillAnimation.calculationMode = kCAAnimationDiscrete
foreground.add(fillAnimation, forKey:nil)

Animate Custom UIView Property in Swift

all!
I have created a circular progress view using CoreGraphics that looks and updates like so:
50%
75%
The class is a UIView class, and it has a variable called 'progress' that determines how much of the circle is filled in.
It works well, but I want to be able to animate changes to the progress variable so that the bar animates smoothly.
I have read from myriad examples that I need to have a CALayer class along with the View class, which I have made, however, it doesn't animate at all.
Two questions:
Can I keep the graphic I drew in CoreGraphics, or do I need to somehow redraw it in CALayer?
My current (attempted) solution crashes towards the bottom at: anim.fromValue = pres.progress. What's up?
class CircleProgressView: UIView {
#IBInspectable var backFillColor: UIColor = UIColor.blueColor()
#IBInspectable var fillColor: UIColor = UIColor.greenColor()
#IBInspectable var strokeColor: UIColor = UIColor.greenColor()
dynamic var progress: CGFloat = 0.00 {
didSet {
self.layer.setValue(progress, forKey: "progress")
}
}
var distToDestination: CGFloat = 10.0
#IBInspectable var arcWidth: CGFloat = 20
#IBInspectable var outlineWidth: CGFloat = 5
override class func layerClass() -> AnyClass {
return CircleProgressLayer.self
}
override func drawRect(rect: CGRect) {
var fillColor = self.fillColor
if distToDestination < 3.0 {
fillColor = UIColor.greenColor()
} else {
fillColor = self.fillColor
}
//Drawing the inside of the container
//Drawing in the container
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let radius: CGFloat = max(bounds.width, bounds.height) - 10
let startAngle: CGFloat = 3 * π / 2
let endAngle: CGFloat = 3 * π / 2 + 2 * π
let path = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = arcWidth
backFillColor.setStroke()
path.stroke()
let fill = UIColor.blueColor().colorWithAlphaComponent(0.15)
fill.setFill()
path.fill()
//Drawing the fill path. Same process
let fillAngleLength = (π) * progress
let fillStartAngle = 3 * π / 2 - fillAngleLength
let fillEndAngle = 3 * π / 2 + fillAngleLength
let fillPath_fill = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: fillStartAngle, endAngle: fillEndAngle, clockwise: true)
fillPath_fill.lineWidth = arcWidth
fillColor.setStroke()
fillPath_fill.stroke()
//Drawing container outline on top
let outlinePath_outer = UIBezierPath(arcCenter: center, radius: radius / 2 - outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let outlinePath_inner = UIBezierPath(arcCenter: center, radius: radius / 2 - arcWidth + outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
outlinePath_outer.lineWidth = outlineWidth
outlinePath_inner.lineWidth = outlineWidth
strokeColor.setStroke()
outlinePath_outer.stroke()
outlinePath_inner.stroke()
}
}
class CircleProgressLayer: CALayer {
#NSManaged var progress: CGFloat
override class func needsDisplayForKey(key: String) -> Bool {
if key == "progress" {
return true
}
return super.needsDisplayForKey(key)
}
override func actionForKey(key: String) -> CAAction? {
if (key == "progress") {
if let pres = self.presentationLayer() {
let anim: CABasicAnimation = CABasicAnimation.init(keyPath: key)
anim.fromValue = pres.progress
anim.duration = 0.2
return anim
}
return super.actionForKey(key)
} else {
return super.actionForKey(key)
}
}
}
Thanks for the help!
Try this out :)
class ViewController: UIViewController {
let progressView = CircleProgressView(frame:CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200))
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.frame = CGRectMake(0, 300, 200, 100)
button.backgroundColor = UIColor.yellowColor()
button.addTarget(self, action: #selector(ViewController.tap), forControlEvents: UIControlEvents.TouchUpInside)
view.addSubview(button)
view.addSubview(progressView)
progressView.progress = 1.0
}
func tap() {
if progressView.progress == 0.5 {
progressView.progress = 1.0
} else {
progressView.progress = 0.5
}
}
}
class CircleProgressView: UIView {
dynamic var progress: CGFloat = 0.00 {
didSet {
let animation = CABasicAnimation()
animation.keyPath = "progress"
animation.fromValue = circleLayer().progress
animation.toValue = progress
animation.duration = Double(0.5)
self.layer.addAnimation(animation, forKey: "progress")
circleLayer().progress = progress
}
}
func circleLayer() -> CircleProgressLayer {
return self.layer as! CircleProgressLayer
}
override class func layerClass() -> AnyClass {
return CircleProgressLayer.self
}
}
class CircleProgressLayer: CALayer {
#NSManaged var progress: CGFloat
override class func needsDisplayForKey(key: String) -> Bool {
if key == "progress" {
return true
}
return super.needsDisplayForKey(key)
}
var backFillColor: UIColor = UIColor.blueColor()
var fillColor: UIColor = UIColor.greenColor()
var strokeColor: UIColor = UIColor.greenColor()
var distToDestination: CGFloat = 10.0
var arcWidth: CGFloat = 20
var outlineWidth: CGFloat = 5
override func drawInContext(ctx: CGContext) {
super.drawInContext(ctx)
UIGraphicsPushContext(ctx)
//Drawing in the container
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let radius: CGFloat = max(bounds.width, bounds.height) - 10
let startAngle: CGFloat = 3 * CGFloat(M_PI) / 2
let endAngle: CGFloat = 3 * CGFloat(M_PI) / 2 + 2 * CGFloat(M_PI)
let path = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = arcWidth
backFillColor.setStroke()
path.stroke()
let fill = UIColor.blueColor().colorWithAlphaComponent(0.15)
fill.setFill()
path.fill()
//Drawing the fill path. Same process
let fillAngleLength = (CGFloat(M_PI)) * progress
let fillStartAngle = 3 * CGFloat(M_PI) / 2 - fillAngleLength
let fillEndAngle = 3 * CGFloat(M_PI) / 2 + fillAngleLength
let fillPath_fill = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: fillStartAngle, endAngle: fillEndAngle, clockwise: true)
fillPath_fill.lineWidth = arcWidth
fillColor.setStroke()
fillPath_fill.stroke()
//Drawing container outline on top
let outlinePath_outer = UIBezierPath(arcCenter: center, radius: radius / 2 - outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let outlinePath_inner = UIBezierPath(arcCenter: center, radius: radius / 2 - arcWidth + outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
outlinePath_outer.lineWidth = outlineWidth
outlinePath_inner.lineWidth = outlineWidth
strokeColor.setStroke()
outlinePath_outer.stroke()
outlinePath_inner.stroke()
UIGraphicsPopContext()
}
}
Whilst AntonTheDev provides a great answer, his solution does not allow you to animate the CircularProgressView in an animation block, so you cant do neat things like:
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut,
animations: {
circularProgress.progress = 0.76
}, completion: nil)
There's a similar question here with a up to date Swift 3 answer based on ideas from the accepted answer and this post.
This is what the final solution looks like.
Swift 3 Solution

Resources