1
votes

I am trying to create a circular progress indicator. Unfortunately, the drawRect routine is only called the first and last times from setNeedsDisplay which does not create the gradual fill pattern that I am looking for. I've created a UIView subclass where I populate a background and then update the draw progress as follows:

- (void)drawRect:(CGRect)rect
{
    // draw background
    CGFloat lineWidth = 5.f;
    UIBezierPath *processBackgroundPath = [UIBezierPath bezierPath];
    processBackgroundPath.lineWidth = lineWidth;
    processBackgroundPath.lineCapStyle = kCGLineCapRound;
    CGPoint center = CGPointMake(self.bounds.size.width / 2, self.bounds.size.width / 2);
    CGFloat radius = (self.bounds.size.width - lineWidth) / 2;
    CGFloat startAngle = (2 * (float)M_PI / 2); // 90 degrees
    CGFloat endAngle = (2 * (float)M_PI) + startAngle;
    [processBackgroundPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
    [[UIColor grayColor] set];
    [processBackgroundPath stroke];

    // draw progress
    UIBezierPath *processPath = [UIBezierPath bezierPath];
    processPath.lineCapStyle = kCGLineCapRound;
    processPath.lineWidth = lineWidth;
    endAngle = (self.progress * 2 * (float)M_PI) + startAngle;
    [processPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
    [[UIColor blackColor] set];
    [processPath stroke];
}

I set the progress variable with the following method:

- (void)setProgress:(float)progress {
    _progress = progress;
    [self setNeedsDisplay];
}

Then I simply call the attached method in my main viewcontroller after assigning a UIView with the above class to my storyboard:

- (void)progressView:(CircularProgress *)activityView loopTime:(CGFloat)duration repeats:(BOOL)repeat {
    float portion = 0.0f;
    while (portion < 1.0f) {
        portion += 1/ (20.0 * duration);
        [activityView setProgress:portion];
        usleep(50000);
    }
}

Again, the [self setNeedsDisplay] only calls drawRect the first and last time through. Thanks in advance for your help.

3

3 Answers

1
votes

As an alternative to NSTimer, I can also recommend using CADisplayLink:

A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display.

Your application creates a new display link, providing a target object and a selector to be called when the screen is updated. Next, your application adds the display link to a run loop.

Once the display link is associated with a run loop, the selector on the target is called when the screen’s contents need to be updated.

This will ensure that your progress view gets redrawn roughly in sync with the device frame rate.

5
votes

usleep(50000) blocks the thread

Use NSTimer instead to updated the progressView.

[NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(updateProgressView) userInfo:nil repeats:YES];
duration = 0;
...


- (void)updateProgressView {
    // Update the progress
  }
}
...
0
votes

Both your drawRect: and your progressView:… methods will be called on the main thread.

setNeedsDisplay: doesn't immediately call drawRect:; it queues it up to be called the next time your application's main thread passes through its run loop. Use an NSTimer or read up on CoreAnimation to see if it provides something more directly applicable: your current solution will eat up a lot of CPU time and will not take advantage of the GPU.