Implementing a clock face in UIView - objective-c

I'm trying to implement a clock face in UIView - without much luck. The problem is that the hands on the clock face end up in totally the wrong position (i.e. pointing to the wrong time). I'm pretty sure that the logic of my code is correct - but clearly there's something significantly wrong.
My test app is an iOS Single View Application. ViewController.h looks like this:
#import <UIKit/UIKit.h>
#interface Clock : UIView {
UIColor* strokeColour;
UIColor* fillColour;
float strokeWidth;
NSDate* clockTime;
CGPoint clockCentre;
float clockRadius;
}
- (id)initWithCentre:(CGPoint)centre
radius:(float)radius
borderWidth:(float)width
facecolour:(UIColor*)fill
handcolour:(UIColor*)handFill
time:(NSDate*)time;
#end
#interface ViewController : UIViewController
#end
and ViewController.m looks like this:
#import "ViewController.h"
#implementation Clock
- (id)initWithCentre:(CGPoint)centre
radius:(float)radius
borderWidth:(float)width
facecolour:(UIColor*)fill
handcolour:(UIColor*)handFill
time:(NSDate*)time {
CGRect frame = CGRectMake(centre.x-radius,
centre.y-radius,
radius*2,
radius*2);
if (self = [super initWithFrame:frame]) {
// Initialization code
strokeColour = fill;
fillColour = handFill;
strokeWidth = width;
clockRadius = radius;
clockTime = time;
clockCentre.x = frame.size.width/2;
clockCentre.y = frame.size.height/2;
}
return self;
}
- (void)drawRect:(CGRect)rect {
double red, green, blue, alpha;
CGRect circleRect;
circleRect.origin.x = rect.origin.x+strokeWidth;
circleRect.origin.y = rect.origin.y+strokeWidth;
circleRect.size.width = rect.size.width - (strokeWidth*2);
circleRect.size.height = rect.size.height - (strokeWidth*2);
CGContextRef contextRef = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(contextRef, strokeWidth);
[fillColour getRed:&red green:&green blue:&blue alpha:&alpha];
CGContextSetRGBFillColor(contextRef, red, green, blue, alpha);
[strokeColour getRed:&red green:&green blue:&blue alpha:&alpha];
CGContextSetRGBStrokeColor(contextRef, red, green, blue, alpha);
CGContextFillEllipseInRect(contextRef, circleRect);
CGContextStrokeEllipseInRect(contextRef, circleRect);
CGContextSetLineWidth(contextRef, strokeWidth);
[strokeColour getRed:&red green:&green blue:&blue alpha:&alpha];
CGContextSetRGBStrokeColor(contextRef, red, green, blue, alpha);
float hourAngle, minuteAngle, secondAngle, angle;
double endX, endY;
NSCalendar *cal = [[NSCalendar alloc]initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSUInteger units = NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
NSDateComponents *components = [cal components:units fromDate:clockTime];
hourAngle = (components.hour /12.0) * M_PI * 2.0;
minuteAngle = (components.minute / 60.0) * M_PI * 2.0;
secondAngle = (components.second / 60.0) * M_PI * 2.0;
//minute hand
angle = minuteAngle;
endX = cos(angle) * (clockRadius*0.85) + clockCentre.x;
endY = sin(angle) * (clockRadius*0.85) + clockCentre.y;
CGContextMoveToPoint(contextRef,clockCentre.x,clockCentre.y);
CGContextAddLineToPoint(contextRef,endX,endY);
CGContextStrokePath(contextRef);
//hour hand
angle = hourAngle;
endX = cos(angle) * (clockRadius*0.65) + clockCentre.x;
endY = sin(angle) * (clockRadius*0.65) + clockCentre.y;
CGContextSetLineWidth(contextRef, strokeWidth*1.5);
CGContextMoveToPoint(contextRef,clockCentre.x,clockCentre.y);
CGContextAddLineToPoint(contextRef,endX,endY);
CGContextStrokePath(contextRef);
}
#end
#interface ViewController ()
#end
#implementation ViewController
- (UIView*)drawClockFaceWithCentre:(CGPoint)centre
radius:(float)radius
time:(NSDate*)time
colour:(UIColor*)colour
backgroundColour:(UIColor*)bgcolour {
UIView* clock = [[Clock alloc]initWithCentre:centre
radius:radius
borderWidth:6.0
facecolour:bgcolour
handcolour:colour
time:time];
clock.backgroundColor = [UIColor clearColor];
return clock;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CGPoint centre;
centre.x=200;
centre.y=200;
[self.view addSubview:[self drawClockFaceWithCentre:centre
radius:100
time:[NSDate date]
colour:[UIColor blackColor]
backgroundColour:[UIColor whiteColor]]];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
I'm sure it's something very simple that I've done wrong - a mistake in my maths. Can anyone see the bug (and any other suggestions for improvements that could be made would be welcome, bearing in mind (of course) that this is just a simple test harness).

As I wrote in the comment, consider dropping doing everything in drawRect and use layers. Also, instead of calculating trigonometry to draw correct path, consider just drawing hands at 12:00 and transform rotate them with correct angle. You could do it like this in init...
CAShapeLayer *faceLayer = [CAShapeLayer layer];
faceLayer.frame = self.bounds;
faceLayer.fillColor = [UIColor whiteColor].CGColor;
faceLayer.strokeColor = [UIColor blackColor].CGColor;
faceLayer.path = [UIBezierPath bezierPathWithOvalInRect:faceLayer.bounds].CGPath;
faceLayer.lineWidth = 3.0;
[self.layer addSublayer:faceLayer];
CGPoint middle = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
NSCalendar *cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSUInteger units = NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
NSDateComponents *components = [cal components:units fromDate:time];
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:middle];
[path addLineToPoint:CGPointMake(middle.x, middle.y-radius*0.5)];
CGFloat hourAreaAngle = (2*M_PI)/12.0;
CAShapeLayer *hourLayer = [CAShapeLayer layer];
hourLayer.frame = self.bounds;
hourLayer.strokeColor = [UIColor redColor].CGColor;
hourLayer.lineWidth = 3.0;
hourLayer.path = path.CGPath;
[self.layer addSublayer:hourLayer];
hourLayer.transform = CATransform3DMakeRotation((components.hour/12.0*(M_PI*2.0))+(hourAreaAngle*(components.minute/60.0)), 0.0, 0.0, 1.0);
[path removeAllPoints];
[path moveToPoint:middle];
[path addLineToPoint:CGPointMake(middle.x, middle.y-radius*0.8)];
CAShapeLayer *minuteLayer = [CAShapeLayer layer];
minuteLayer.frame = self.bounds;
minuteLayer.strokeColor = [UIColor blueColor].CGColor;
minuteLayer.lineWidth = 2.0;
minuteLayer.path = path.CGPath;
[self.layer addSublayer:minuteLayer];
minuteLayer.transform = CATransform3DMakeRotation(components.minute/60.0*(M_PI*2.0), 0.0, 0.0, 1.0);
[path removeAllPoints];
[path moveToPoint:middle];
[path addLineToPoint:CGPointMake(middle.x, middle.y-radius)];
CAShapeLayer *secondsLayer = [CAShapeLayer layer];
secondsLayer.frame = self.bounds;
secondsLayer.strokeColor = [UIColor greenColor].CGColor;
secondsLayer.lineWidth = 1.0;
secondsLayer.path = path.CGPath;
[self.layer addSublayer:secondsLayer];
secondsLayer.transform = CATransform3DMakeRotation(components.second/60.0*(M_PI*2.0), 0.0, 0.0, 1.0);
Note I put my colours and line widths because I'm lazy and didn't want to check your code :P.
If you want to update hands later, just store hands layers as variables and transform rotate them again (just remember to first transform to identity!).

Related

UIBezier Path Refusing to Draw

I'm trying to draw a arch using UIBezierPath. There are no errors or warnings and I cannot see any obvious bugs but it just will not draw. This is my code:
- (void)drawRect:(CGRect)rect {
CGFloat a = 10;
CGFloat width = CGRectGetWidth(self.bounds);
CGFloat height = CGRectGetHeight(self.bounds);
CGPoint center = CGPointMake(width/2, height/2);
CGFloat radius = MAX(width, height);
CGFloat archWidth = 18;
CGFloat archLengthPerA = 5;
CGFloat startAngle = 3 * M_PI /2;
CGFloat endAngle = archLengthPerA * a + startAngle;
UIBezierPath* path = [UIBezierPath bezierPath];
[path addArcWithCenter:center
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES];
path.lineWidth = archWidth;
[[UIColor colorWithRed:255 green:17 blue:0 alpha:1.0] setStroke];
[path stroke];
}
I put it in a separate view to test and it crashed on _myString so is that anything to do with it? I tried adding a breakpoint to see if it worked without it but then it crashed again. Any ideas why it's not working?
Your value of radius is too large, so the arc is drawn outside of the view.
Make it smaller. For instance:
CGFloat radius = MIN(width/2, height/2);
Also, your colour values are incorrect:
[[UIColor colorWithRed:255 green:17 blue:0 alpha:1.0] setStroke];
The colour components of that class method should be CGFloat with values between 0.0 and 1.0

Move a UIImage at a angle with acceleration

I have a series of UIImageViews all hooked up in IBOutlet collection that I would like to move at whatever angle they are facing at a set acceleration speed.
The first thing I do is randomly put them at an angle (ie: 35, 90, 70, 270 degrees) and now I would like to move them.
The images are icons of planes and I would like to move each plane icon at a certain angle (whatever angle they are currently facing) and at a certain acceleration (say 2-4 pixels or something).
The only issue is I am not really sure how to move a UIImage at a given angle at a given acceleration.
Is there are a way to do this with Core Animation?
Many thanks
Update:
I rewrote the method with seperate Objects to handle the Airplanes themselves and try to use UIBezierPath;
// Angles for airplane icons
VICAirplane *plane1 = [[VICAirplane alloc] initWithX:48 withY:104 withAngle:140 withSpeed:5];
VICAirplane *plane2 = [[VICAirplane alloc] initWithX:50 withY:250 withAngle:60 withSpeed:7];
VICAirplane *plane3 = [[VICAirplane alloc] initWithX:280 withY:75 withAngle:240 withSpeed:12];
VICAirplane *plane4 = [[VICAirplane alloc] initWithX:230 withY:270 withAngle:120 withSpeed:3];
VICAirplane *plane5 = [[VICAirplane alloc] initWithX:148 withY:225 withAngle:85 withSpeed:10];
VICAirplane *plane6 = [[VICAirplane alloc] initWithX:175 withY:225 withAngle:85 withSpeed:8];
self.airplanes = #[plane1,plane2,plane3,plane4,plane5,plane6];
UIImage *sprite = [UIImage imageNamed:#"Plane Shadow"];
CGFloat spriteWidth = sprite.size.width;
CGFloat spriteHeight = sprite.size.height;
for (VICAirplane *planeObj in self.airplanes) {
CALayer *plane = [CALayer layer];
plane.bounds = CGRectMake(0, 0, spriteWidth, spriteHeight);
plane.position = CGPointMake(planeObj.x, planeObj.y);
plane.contents = (id)(sprite.CGImage);
CGAffineTransform rotateTransform = CGAffineTransformMakeRotation(planeObj.angle);
[plane setAffineTransform:rotateTransform];
[self.view.layer addSublayer:plane];
// Animate it
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(100.0, 100.0)];
[path addLineToPoint:CGPointMake(200.0, 200.0)];
[path addLineToPoint:CGPointMake(100.0, 300.0)];
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
anim.path = path.CGPath;
anim.rotationMode = kCAAnimationRotateAuto;
anim.repeatCount = HUGE_VALF;
anim.duration = 8.0;
anim.autoreverses = YES;
[plane addAnimation:anim forKey:#"race"];
}
What I'd like to do is generate UIBezierPath based off the angle of each plane and move it at the speed
but I don't think moveToPoint will help solve this
Thanks
I have a potental alternative solution which I am now using/working on;
// Angles for airplane icons
_sprite = [UIImage imageNamed:#"Plane"];
CGFloat spriteWidth = _sprite.size.width;
CGFloat spriteHeight = _sprite.size.height;
self.velocity=1.0;
//Random x&y points
CGFloat x;
CGFloat y;
//VICAirplane *plane1 = [[VICAirplane alloc] initWithX:48 withY:104 withAngle:0 withSpeed:10];
x = (CGFloat) (arc4random() % (int) self.view.frame.size.width);
y = (CGFloat) (arc4random() % (int) self.view.frame.size.height);
UIImageView *theBug = [[UIImageView alloc] initWithFrame:CGRectMake(x, y, spriteWidth, spriteHeight)];
[theBug setBackgroundColor:[UIColor redColor]];
[theBug setImage:_sprite];
[self.view addSubview:theBug];
double delayInSeconds = 1;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
//Calculate the angle to rotate
CGFloat newX = (CGFloat) (arc4random() % (int) self.view.frame.size.width);
CGFloat newY = (CGFloat) (arc4random() % (int) self.view.frame.size.height);
CGPoint rotateToLocation = CGPointMake(newX, newY);
theBug.layer.anchorPoint = CGPointMake(0.5, 0);
float angleToRotate = atan2f(theBug.transform.tx-newX, theBug.transform.ty - newY);
if (theBug.transform.tx < newX)
angleToRotate *= 1;
[UIView animateWithDuration:4
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
theBug.transform = CGAffineTransformRotate(theBug.transform, angleToRotate);
} completion:^(BOOL finished) {
[UIView animateWithDuration:2
delay:0.0
options:UIViewAnimationOptionCurveLinear
animations:^{
theBug.center = rotateToLocation;
} completion:^(BOOL finished) {
NSLog(#"Finished");
}];
}];
});
This is my solution for the issue. Many thanks.

Objective-C Moving object along curved path

I've looked at other SO answers to this quesiton, particularly Brad Larson's but I am still not getting any results. I'm not sure if I am approaching this the correct way. In my app, when a user touches the screen I capture the point and create an arc from a fixed point to that touch point. I then want to move a UIView along that arc.
Here's my code:
ViewController.m
//method to "shoot" object - KIP_Projectile creates the arc, KIP_Character creates the object I want to move along the arc
...
//get arc for trajectory
KIP_Projectile* vThisProjectile = [[KIP_Projectile alloc] initWithFrame:CGRectMake(51.0, fatElvisCenterPoint-30.0, touchPoint.x, 60.0)];
vThisProjectile.backgroundColor = [UIColor clearColor];
[self.view addSubview:vThisProjectile];
...
KIP_Character* thisChar = [[KIP_Character alloc] initWithFrame:CGRectMake(51, objCenterPoint-5, imgThisChar.size.width, imgThisChar.size.height)];
thisChar.backgroundColor = [UIColor clearColor];
thisChar.charID = charID;
thisChar.charType = 2;
thisChar.strCharType = #"Projectile";
thisChar.imgMyImage = imgThisChar;
thisChar.myArc = vThisProjectile;
[thisChar buildImage];
[thisChar traceArc];
in KIP_Projectile I build the arc using this code:
- (CGMutablePathRef) createArcPathFromBottomOfRect : (CGRect) rect : (CGFloat) arcHeight {
CGRect arcRect = CGRectMake(rect.origin.x, rect.origin.y + rect.size.height - arcHeight, rect.size.width, arcHeight);
CGFloat arcRadius = (arcRect.size.height/2) + (pow(arcRect.size.width, 2) / (8*arcRect.size.height));
CGPoint arcCenter = CGPointMake(arcRect.origin.x + arcRect.size.width/2, arcRect.origin.y + arcRadius);
CGFloat angle = acos(arcRect.size.width / (2*arcRadius));
CGFloat startAngle = radians(180) + angle;
CGFloat endAngle = radians(360) - angle;
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, NULL, arcCenter.x, arcCenter.y, arcRadius, startAngle, endAngle, 0);
return path;
}
- (void)drawRect:(CGRect)rect {
CGContextRef currentContext = UIGraphicsGetCurrentContext();
_myArcPath = [self createArcPathFromBottomOfRect:self.bounds:30.0];
CGContextSetLineWidth(currentContext, 1);
CGFloat red[4] = {1.0f, 0.0f, 0.0f, 1.0f};
CGContextSetStrokeColor(currentContext, red);
CGContextAddPath(currentContext, _myArcPath);
CGContextStrokePath(currentContext);
}
Works fine. The arc is displayed with a red stroke on the screen.
In KIP_Character, which has been passed it's relevant arc, I am using this code but getting no results.
- (void) traceArc {
CGMutablePathRef myArc = _myArc.myArcPath;
// Set up path movement
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.path = myArc;
CGPathRelease(myArc);
[self.layer addAnimation:pathAnimation forKey:#"savingAnimation"];
}
Any help here would be appreciated.

Blurred Screenshot of a view being drawn by UIBezierPath

I'm drawing my graph view using UIBezierPathmethods and coretext. I use addQuadCurveToPoint:controlPoint: method to draw curves on graph. I also use CATiledLayer for the purpose of rendering graph with large data set on x axis. I draw my whole graph in an image context and in drawrect: method of my view I draw this image in my whole view. Following is my code.
- (void)drawImage{
UIGraphicsBeginImageContextWithOptions(self.frame.size, NO, 0.0);
// Draw Curves
[self drawDiagonal];
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
[screenshot retain];
UIGraphicsEndImageContext();
}
- (void)drawRect:(CGRect)rect{
NSLog(#"Draw iN rect with Bounds: %#",NSStringFromCGRect(rect));
[screenshot drawInRect:self.frame];
}
However in screenshot the curves drawn between two points are not smooth. I've also set the Render with Antialiasing to YES in my info plist. Please see screenshot.
We'd have to see how you construct the UIBezierPath, but in my experience, for smooth curves, the key issue is whether the slope of the line between a curve's control point and the end point of that particular segment of the curve is equal to the slope between the next segment of the curve's start point and its control point. I find that easier to draw general smoooth curves using addCurveToPoint rather than addQuadCurveToPoint, so that I can adjust the starting and ending control points to satisfy this criterion more generally.
To illustrate this point, the way I usually draw UIBezierPath curves is to have an array of points on the curve, and the angle that the curve should take at that point, and then the "weight" of the addCurveToPoint control points (i.e. how far out the control points should be). Thus, I use those parameters to dictate the second control point of a UIBezierPath and the first controlPoint of the next segment of the UIBezierPath. So, for example:
#interface BezierPoint : NSObject
#property CGPoint point;
#property CGFloat angle;
#property CGFloat weight;
#end
#implementation BezierPoint
- (id)initWithPoint:(CGPoint)point angle:(CGFloat)angle weight:(CGFloat)weight
{
self = [super init];
if (self)
{
self.point = point;
self.angle = angle;
self.weight = weight;
}
return self;
}
#end
And then, an example of how I use that:
- (void)loadBezierPointsArray
{
// clearly, you'd do whatever is appropriate for your chart.
// this is just a unclosed loop. But it illustrates the idea.
CGPoint startPoint = CGPointMake(self.view.frame.size.width / 2.0, 50);
_bezierPoints = [NSMutableArray arrayWithObjects:
[[BezierPoint alloc] initWithPoint:CGPointMake(startPoint.x, startPoint.y)
angle:M_PI_2 * 0.05
weight:100.0 / 1.7],
[[BezierPoint alloc] initWithPoint:CGPointMake(startPoint.x + 100.0, startPoint.y + 70.0)
angle:M_PI_2
weight:70.0 / 1.7],
[[BezierPoint alloc] initWithPoint:CGPointMake(startPoint.x, startPoint.y + 140.0)
angle:M_PI
weight:100.0 / 1.7],
[[BezierPoint alloc] initWithPoint:CGPointMake(startPoint.x - 100.0, startPoint.y + 70.0)
angle:M_PI_2 * 3.0
weight:70.0 / 1.7],
[[BezierPoint alloc] initWithPoint:CGPointMake(startPoint.x + 10.0, startPoint.y + 10)
angle:0.0
weight:100.0 / 1.7],
nil];
}
- (CGPoint)calculateForwardControlPoint:(NSUInteger)index
{
BezierPoint *bezierPoint = _bezierPoints[index];
return CGPointMake(bezierPoint.point.x + cosf(bezierPoint.angle) * bezierPoint.weight,
bezierPoint.point.y + sinf(bezierPoint.angle) * bezierPoint.weight);
}
- (CGPoint)calculateReverseControlPoint:(NSUInteger)index
{
BezierPoint *bezierPoint = _bezierPoints[index];
return CGPointMake(bezierPoint.point.x - cosf(bezierPoint.angle) * bezierPoint.weight,
bezierPoint.point.y - sinf(bezierPoint.angle) * bezierPoint.weight);
}
- (UIBezierPath *)bezierPath
{
UIBezierPath *path = [UIBezierPath bezierPath];
BezierPoint *bezierPoint = _bezierPoints[0];
[path moveToPoint:bezierPoint.point];
for (NSInteger i = 1; i < [_bezierPoints count]; i++)
{
bezierPoint = _bezierPoints[i];
[path addCurveToPoint:bezierPoint.point
controlPoint1:[self calculateForwardControlPoint:i - 1]
controlPoint2:[self calculateReverseControlPoint:i]];
}
return path;
}
When I render this into a UIImage (using the code below), I don't see any softening of the image, but admittedly the images are not identical. (I'm comparing the image rendered by capture against that which I capture manually with a screen snapshot by pressing power and home buttons on my physical device at the same time.)
If you're seeing some softening, I would suggest renderInContext (as shown below). I wonder if you writing the image as JPG (which is lossy). Maybe try PNG, if you used JPG.
- (void)drawBezier
{
UIBezierPath *path = [self bezierPath];
CAShapeLayer *oval = [[CAShapeLayer alloc] init];
oval.path = path.CGPath;
oval.strokeColor = [UIColor redColor].CGColor;
oval.fillColor = [UIColor clearColor].CGColor;
oval.lineWidth = 5.0;
oval.strokeStart = 0.0;
oval.strokeEnd = 1.0;
[self.view.layer addSublayer:oval];
}
- (void)capture
{
UIGraphicsBeginImageContextWithOptions(self.view.frame.size, NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
[self.view.layer renderInContext:context];
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// save the image
NSData *data = UIImagePNGRepresentation(screenshot);
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *imagePath = [documentsPath stringByAppendingPathComponent:#"image.png"];
[data writeToFile:imagePath atomically:YES];
// send it to myself so I can look at the file
NSURL *url = [NSURL fileURLWithPath:imagePath];
UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:#[url]
applicationActivities:nil];
[self presentViewController:controller animated:YES completion:nil];
}

Circular Progress Bars in IOS

I want to create a circular progress bar like the following:
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!
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;
}
}
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];
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
You can check out my lib MBCircularProgressBar
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.
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)
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.