This is part one of a two-part series of blogposts. It follows other recent blogposts on CGContext and the drawing of circles and regular polygons.
Drawing the circular background
To begin drawing a clock face the first ingredient is the circular, lowermost element.override func drawRect(rect:CGRect) { // obtain context let ctx = UIGraphicsGetCurrentContext() // decide on radius let rad = CGRectGetWidth(rect)/3.5 let endAngle = CGFloat(2*M_PI) // add the circle to the context CGContextAddArc(ctx, CGRectGetMidX(rect), CGRectGetMidY(rect), rad, 0, endAngle, 1) // set fill color CGContextSetFillColorWithColor(ctx,UIColor.grayColor().CGColor) // set stroke color CGContextSetStrokeColorWithColor(ctx,UIColor.whiteColor().CGColor) // set line width CGContextSetLineWidth(ctx, 4.0) // use to fill and stroke path (see http://stackoverflow.com/questions/13526046/cant-stroke-path-after-filling-it ) // draw the path CGContextDrawPath(ctx, kCGPathFillStroke); }The comments explain here the different steps in the creation of the circle within a UIView subclass.
Adding the second markers using translation and rotation
In this previous post, I explained the process of translating and rotating a context. I now want to write code to demonstrate how this can be used to draw second markers around a clock face.
First a function is required to do the drawing based on the co-ordinates we're going to supply to it.
func degree2radian(a:CGFloat)->CGFloat { let b = CGFloat(M_PI) * a/180 return b } func drawSecondMarker(#ctx:CGContextRef, #x:CGFloat, #y:CGFloat, #radius:CGFloat, #color:UIColor) { // generate a path let path = CGPathCreateMutable() // move to starting point on edge of circle CGPathMoveToPoint(path, nil, radius, 0) // draw line of required length CGPathAddLineToPoint(path, nil, x, y) // close subpath CGPathCloseSubpath(path) // add the path to the context CGContextAddPath(ctx, path) // set the line width CGContextSetLineWidth(ctx, 1.5) // set the line color CGContextSetStrokeColorWithColor(ctx,color.CGColor) // draw the line CGContextStrokePath(ctx) }Next we need to draw sixty of these lines:
for i in 1...60 { // save the original position and origin CGContextSaveGState(ctx) // make translation CGContextTranslateCTM(ctx, CGRectGetMidX(rect), CGRectGetMidY(rect)) // make rotation CGContextRotateCTM(ctx, degree2radian(CGFloat(i)*6)) if i % 5 == 0 { // if an hour position we want a line slightly longer drawSecondMarker(ctx: ctx, x: rad-15, y:0, radius:rad, color: UIColor.whiteColor()) } else { drawSecondMarker(ctx: ctx, x: rad-10, y:0, radius:rad, color: UIColor.whiteColor()) } // restore state before next translation CGContextRestoreGState(ctx) }So the entire code looks like this and the result when built and run looks like this:
Building a path using points along the circumference of the circle
The other way to position these markers would be divide the circumference of the circle into 60 equal points and to build an array of CGPoints that can be used to position the second markers. The advantage of which is that the function can be reused to place the numbers around the face as well.
To calculate the points use the following code:
To calculate the points use the following code:
func degree2radian(a:CGFloat)->CGFloat { let b = CGFloat(M_PI) * a/180 return b } func circleCircumferencePoints(sides:Int,x:CGFloat,y:CGFloat,radius:CGFloat,adjustment:CGFloat=0)->[CGPoint] { let angle = degree2radian(360/CGFloat(sides)) let cx = x // x origin let cy = y // y origin let r = radius // radius of circle var i = sides var points = [CGPoint]() while points.count <= sides { let xpo = cx - r * cos(angle * CGFloat(i)+degree2radian(adjustment)) let ypo = cy - r * sin(angle * CGFloat(i)+degree2radian(adjustment)) points.append(CGPoint(x: xpo, y: ypo)) i--; } return points }To then draw the second markers a further function is necessary:
func secondMarkers(#ctx:CGContextRef, #x:CGFloat, #y:CGFloat, #radius:CGFloat, #sides:Int, #color:UIColor) { // retrieve points let points = circleCircumferencePoints(sides,x,y,radius) // create path let path = CGPathCreateMutable() // determine length of marker as a fraction of the total radius var divider:CGFloat = 1/16 for p in enumerate(points) { if p.index % 5 == 0 { divider = 1/8 } else { divider = 1/16 } let xn = p.element.x + divider*(x-p.element.x) let yn = p.element.y + divider*(y-p.element.y) // build path CGPathMoveToPoint(path, nil, p.element.x, p.element.y) CGPathAddLineToPoint(path, nil, xn, yn) CGPathCloseSubpath(path) // add path to context CGContextAddPath(ctx, path) } // set path color let cgcolor = color.CGColor CGContextSetStrokeColorWithColor(ctx,cgcolor) CGContextSetLineWidth(ctx, 3.0) CGContextStrokePath(ctx) }Finally the secondMarkers() function must be called from within the drawRect() of the UIView subclass:
secondMarkers(ctx: ctx, x: CGRectGetMidX(rect), y: CGRectGetMidY(rect), radius: rad, sides: 60, color: UIColor.whiteColor())
Adding text to the clock face
Finding the points at which to position the text the circleCircumferencePoints() function is used. While the text itself will be a series of CFAttributedStrings, which take a dictionary with the same range of keys as an NSAttributedString:
Once the text is drawn, the result looks like this:
func drawText(#rect:CGRect, #ctx:CGContextRef, #x:CGFloat, #y:CGFloat, #radius:CGFloat, #sides:Int, #color:UIColor) { // Flip text co-ordinate space, see: http://blog.spacemanlabs.com/2011/08/quick-tip-drawing-core-text-right-side-up/ CGContextTranslateCTM(ctx, 0.0, CGRectGetHeight(rect)) CGContextScaleCTM(ctx, 1.0, -1.0) // dictates on how inset the ring of numbers will be let inset:CGFloat = radius/3.5 // An adjustment of 270 degrees to position numbers correctly let points = circleCircumferencePoints(sides,x,y,radius-inset,adjustment:270) let path = CGPathCreateMutable() for p in enumerate(points) { if p.index > 0 { // Font name must be written exactly the same as the system stores it (some names are hyphenated, some aren't) and must exist on the user's device. Otherwise there will be a crash. (In real use checks and fallbacks would be created.) For a list of iOS 7 fonts see here: http://support.apple.com/en-us/ht5878 let aFont = UIFont(name: "Optima-Bold", size: radius/5) // create a dictionary of attributes to be applied to the string let attr:CFDictionaryRef = [NSFontAttributeName:aFont!,NSForegroundColorAttributeName:UIColor.whiteColor()] // create the attributed string let text = CFAttributedStringCreate(nil, p.index.description, attr) // create the line of text let line = CTLineCreateWithAttributedString(text) // retrieve the bounds of the text let bounds = CTLineGetBoundsWithOptions(line, CTLineBoundsOptions.UseOpticalBounds) // set the line width to stroke the text with CGContextSetLineWidth(ctx, 1.5) // set the drawing mode to stroke CGContextSetTextDrawingMode(ctx, kCGTextStroke) // Set text position and draw the line into the graphics context, text length and height is adjusted for let xn = p.element.x - bounds.width/2 let yn = p.element.y - bounds.midY CGContextSetTextPosition(ctx, xn, yn) // the line of text is drawn - see https://developer.apple.com/library/ios/DOCUMENTATION/StringsTextFonts/Conceptual/CoreText_Programming/LayoutOperations/LayoutOperations.html // draw the line of text CTLineDraw(line, ctx) } } }
Once the text is drawn, the result looks like this:
Conclusion
The fixed-position components of the clock are now complete and the entire code can be found in this Gist. Next time I'll work on drawing and animating the hands.
Note: currently the code will dynamically resize its elements based on the device, but when testing on an iPad you will need to start the code running while the device is in the portrait position, or alternatively alter the size of the view added to the UIViewController.
Note: currently the code will dynamically resize its elements based on the device, but when testing on an iPad you will need to start the code running while the device is in the portrait position, or alternatively alter the size of the view added to the UIViewController.
Comments
Post a Comment