Sunday, July 1, 2018

Swift How to draw a clock face using CoreGraphics and CoreText Part 1

Swift How to draw a clock face using CoreGraphics and CoreText Part 1



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 were 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:
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:
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 arent) and must exist on the users 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 Ill 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.


Follow @sketchytech
Endorse on Coderwall


visit link download