57

I want to create a circular progress bar like the following:

Circular progress bar

How can I do that using Objective-C and Cocoa?

How I started doing it was creating a UIView and editing the drawRect, but I am bit lost. Any help would be greatly appreciated.

Thanks!

Sam Spencer
  • 8,492
  • 12
  • 76
  • 133
Kermit the Frog
  • 3,949
  • 7
  • 31
  • 39
  • Just as an aside you can include images in your question. It saves us jumping to another website to look at them. – Peter M Nov 28 '12 at 00:32
  • Only users with a high enough rep can include images, not sure 31 is high enough. – WDUK Nov 28 '12 at 14:21
  • @WDuk it must be a low level as I am sure I saw someone with a sub 100 rep posting images. I just checked meta-stack overflow, and they suggest that a rep of 10 is the minimum for posting images. – Peter M Nov 28 '12 at 16:59
  • Done it myself, no worries. – WDUK Nov 28 '12 at 17:04
  • This is exactly what u looking for: https://github.com/marshluca/AudioPlayer You can also refer to some Sources : https://github.com/lipka/LLACircularProgressView – Sankalp S Raul Nov 17 '13 at 11:12
  • You can check my library: https://github.com/PavelKatunin/DownloadButton It contains `PKCircleProgressView` which could be useful for you. – BergP Jun 01 '15 at 20:28
  • Check out this control – https://github.com/maxkonovalov/MKRingProgressView It is highly customizable and is able to produce results like this: [![enter image description here](http://i.stack.imgur.com/8OQIi.png)](http://i.stack.imgur.com/8OQIi.png) – maxkonovalov Oct 23 '15 at 09:51

7 Answers7

68

The basic concept is to use the UIBezierPath class to your advantage. You are able to draw arcs, which achieve the effect you're after. I've only had half an hour or so to have a crack at this, but my attempt is below.

Very rudimentary, it simply uses a stroke on the path, but here we go. You can alter/modify this to your exact needs, but the logic to do the arc countdown will be very similar.

In the view class:

@interface TestView () {
    CGFloat startAngle;
    CGFloat endAngle;
}

@end

@implementation TestView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        self.backgroundColor = [UIColor whiteColor];

        // Determine our start and stop angles for the arc (in radians)
        startAngle = M_PI * 1.5;
        endAngle = startAngle + (M_PI * 2);

    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    // Display our percentage as a string
    NSString* textContent = [NSString stringWithFormat:@"%d", self.percent];

    UIBezierPath* bezierPath = [UIBezierPath bezierPath];

    // Create our arc, with the correct angles
    [bezierPath addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2) 
                          radius:130 
                      startAngle:startAngle
                        endAngle:(endAngle - startAngle) * (_percent / 100.0) + startAngle
                       clockwise:YES];

    // Set the display for the path, and stroke it
    bezierPath.lineWidth = 20;
    [[UIColor redColor] setStroke];
    [bezierPath stroke];

    // Text Drawing
    CGRect textRect = CGRectMake((rect.size.width / 2.0) - 71/2.0, (rect.size.height / 2.0) - 45/2.0, 71, 45);
    [[UIColor blackColor] setFill];
    [textContent drawInRect: textRect withFont: [UIFont fontWithName: @"Helvetica-Bold" size: 42.5] lineBreakMode: NSLineBreakByWordWrapping alignment: NSTextAlignmentCenter];
}

For the view controller:

@interface ViewController () {    
    TestView* m_testView;
    NSTimer* m_timer;
}

@end

- (void)viewDidLoad
{
    // Init our view
    [super viewDidLoad];
    m_testView = [[TestView alloc] initWithFrame:self.view.bounds];
    m_testView.percent = 100;
    [self.view addSubview:m_testView];
}

- (void)viewDidAppear:(BOOL)animated
{
    // Kick off a timer to count it down
    m_timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(decrementSpin) userInfo:nil repeats:YES];
}

- (void)decrementSpin
{
    // If we can decrement our percentage, do so, and redraw the view
    if (m_testView.percent > 0) {
        m_testView.percent = m_testView.percent - 1;
        [m_testView setNeedsDisplay];
    }
    else {
       [m_timer invalidate];
       m_timer = nil;
    }
}
WDUK
  • 18,870
  • 3
  • 64
  • 72
  • do you know how I can than put the background images to make it look like the picture above? – Kermit the Frog Dec 02 '12 at 04:39
  • How do I get this to have ronnded ends? – Siriss Sep 25 '14 at 18:24
  • @Siriss Have you tried changing `lineCapStyle` on the `UIBezierPath`? – WDUK Oct 15 '14 at 13:28
  • Thanks! Yes, i finally figured it out, just forgot to come back and update this. – Siriss Oct 15 '14 at 13:51
  • Great sample code. I was able to create exactly what I needed based on this sample. Thanks!! – Rickster Nov 15 '14 at 00:01
  • 4
    Great post, thank you! Some remarks for absolute beginners:
    - `[self.view addSubview:m_webView];` should of course be `[self.view addSubview: m_testView];` - TestView.h should look like this:
    `#import @interface UICircle : UIView @property (nonatomic) double percent; @end`
    – fat32 Nov 23 '14 at 18:56
  • @WDUK : great sample code helped a lot thanx. I want to draw a circle which is supposed to be like filling like a circular progress-bar exactly like like above image. i could draw a circle but when i try to draw on it with 25 or 50 % progress the new one overwrites the previous one. How do i draw 2 circles? – Mak13 Feb 09 '15 at 04:52
23

My example with magic numbers (for better understanding):

  CAShapeLayer *circle = [CAShapeLayer layer];
  circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(29, 29) radius:27 startAngle:-M_PI_2 endAngle:2 * M_PI - M_PI_2 clockwise:YES].CGPath;
  circle.fillColor = [UIColor clearColor].CGColor;
  circle.strokeColor = [UIColor greenColor].CGColor;
  circle.lineWidth = 4;

  CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
  animation.duration = 10;
  animation.removedOnCompletion = NO;
  animation.fromValue = @(0);
  animation.toValue = @(1);
  animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
  [circle addAnimation:animation forKey:@"drawCircleAnimation"];

  [imageCircle.layer.sublayers makeObjectsPerformSelector:@selector(removeFromSuperlayer)];
  [imageCircle.layer addSublayer:circle];
Darkngs
  • 6,381
  • 5
  • 25
  • 30
  • how would you get the lead CGPoint as it animates around? – whyoz Nov 19 '14 at 09:10
  • @whyoz, I used method **bezierPathWithArcCenter** that draws arc at center! – Darkngs Nov 19 '14 at 11:09
  • well, I know that! Haha, I'm trying to get more info outta ya if you have an idea how to do that, if I wanted to track that point as it moved around center. So get the "startPoint" in the form of a CGPoint from the startAngle's first point..thought there might be a quick property to grab.. – whyoz Nov 19 '14 at 23:40
  • 1
    @whyoz, You want track **CGPoint** in real time? I don't know easy way to do this. But you can calculate this point. Something like this: 1 Get current angle - [current time seconds - start time seconds] * 360 / [duration seconds]. 2 We know angle and radius. We need calculate point on circle. x = radius * sin(current angle), y = radius * cos(current angle). I hope this will help you. – Darkngs Nov 21 '14 at 11:31
  • Can we show this animation with progress value instead of time duration? – Jagdev Sendhav Jan 17 '18 at 08:16
  • @UserDev, yes, you can update CAShapeLayer object every time when your progress will be changed. – Darkngs Jan 17 '18 at 17:51
  • @Darkngs, nice code, but i want circle based on percent. how to do it. – Naresh Nov 14 '19 at 12:21
21

I have implemented a simple library for iOS doing just that. It's based on the UILabel class so you can display whatever you want inside your progress bar, but you can also leave it empty.

Once initialized, you only have one line of code to set the progress :

[_myProgressLabel setProgress:(50/100))];

The library is named KAProgressLabel

Alexis C.
  • 4,898
  • 1
  • 31
  • 43
13

You can check out my lib MBCircularProgressBar

Mati Bot
  • 797
  • 6
  • 13
12

For Swift use this,

let circle = UIView(frame: CGRectMake(0,0, 100, 100))

circle.layoutIfNeeded()

let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius : CGFloat = circle.bounds.width / 2 * 0.83

var circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.5 * M_PI), endAngle: CGFloat(1.5 * M_PI), clockwise: true    )

let progressCircle = CAShapeLayer()
progressCircle.path = circlePath.CGPath
progressCircle.strokeColor = UIColor.greenColor().CGColor
progressCircle.fillColor = UIColor.clearColor().CGColor
progressCircle.lineWidth = 1.5
progressCircle.strokeStart = 0
progressCircle.strokeEnd = 0.22

circle.layer.addSublayer(progressCircle)

self.view.addSubview(circle)

Reference: See Here.

luk2302
  • 55,258
  • 23
  • 97
  • 137
Mohammad Zaid Pathan
  • 16,304
  • 7
  • 99
  • 130
10

Swift 3 use this,

CAShapeLayer with Animation : Continue with Zaid Pathan ans.

    let circle = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))

    circle.layoutIfNeeded()

    var progressCircle = CAShapeLayer()

    let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
    let circleRadius : CGFloat = circle.bounds.width / 2 * 0.83

    let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.5 * M_PI), endAngle: CGFloat(1.5 * M_PI), clockwise: true    )

    progressCircle = CAShapeLayer ()
    progressCircle.path = circlePath.cgPath
    progressCircle.strokeColor = UIColor.green.cgColor
    progressCircle.fillColor = UIColor.clear.cgColor
    progressCircle.lineWidth = 2.5
    progressCircle.strokeStart = 0
    progressCircle.strokeEnd = 1.0
     circle.layer.addSublayer(progressCircle)


    let animation = CABasicAnimation(keyPath: "strokeEnd")
    animation.fromValue = 0
    animation.toValue = 1.0
    animation.duration = 5.0
    animation.fillMode = kCAFillModeForwards
    animation.isRemovedOnCompletion = false
     progressCircle.add(animation, forKey: "ani")

    self.view.addSubview(circle)
Apollo
  • 8,874
  • 32
  • 104
  • 192
Chandramani
  • 871
  • 1
  • 12
  • 11
1

Here a Swift example of how to make a simple, not closed(to leave space for long numbers) circular progress bar with rounded corners and animation.

open_circular_progress_bar.jpg

func drawBackRingFittingInsideView(lineWidth: CGFloat, lineColor: UIColor) {

    let halfSize:CGFloat = min( bounds.size.width/2, bounds.size.height/2)

    let desiredLineWidth:CGFloat = lineWidth

    let circle = CGFloat(Double.pi * 2)

    let startAngle = CGFloat(circle * 0.1)

    let endAngle = circle – startAngle

    let circlePath = UIBezierPath(

        arcCenter: CGPoint(x:halfSize, y:halfSize),

        radius: CGFloat( halfSize – (desiredLineWidth/2) ),

        startAngle: startAngle,

        endAngle: endAngle,

        clockwise: true)

    let shapeBackLayer = CAShapeLayer()

        shapeBackLayer.path = circlePath.cgPath

        shapeBackLayer.fillColor = UIColor.clear.cgColor

        shapeBackLayer.strokeColor = lineColor.cgColor

        shapeBackLayer.lineWidth = desiredLineWidth

        shapeBackLayer.lineCap = .round

    layer.addSublayer(shapeBackLayer)

}

And the animation function.

 func animateCircle(duration: TimeInterval) {

    let animation = CABasicAnimation(keyPath: “strokeEnd”)

    animation.duration = duration

    animation.fromValue = 0

    animation.toValue = 1

    animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)        

    shapeLayer.strokeEnd = 1.0

    shapeLayer.add(animation, forKey: “animateCircle”)

}

There is a good blog with examples.