Having owned an Apple Watch for over a year, I’ve grown a little bored with the available watch faces. So I was very impressed with the new, special edition Hermes watch face. This is quite different to any of the other faces on the Apple Watch. Unfortunately, it’s only available to new purchasers who buy the brand new Hermes Apple Watch – starting at £1000.
Having written my first Watch App recently, I thought I’d have a go at writing a Watch Face App. Apple WatchKit doesn’t officially support writing watch faces, so this would have to be an ordinary App that happens to display the time. There are plenty of custom faces you can use as wall-paper to create an attractive digital watch, but I wanted the analogue look.
The approach I took was:
- Start with an image e.g. from the custom faces library
- Look up how to get the current time in the WatchKit API
- Find out how to draw graphics on top of an image
- Work out the maths to draw the hands in the right place
- Work out how to render proper watch hands
I already blogged about how to get the current time and how to draw graphics on top of an image. So if you followed along, you can replace your Treasure Map image with a watch face image and use the createContext(), drawLine() and applyContextToImage() methods to draw the hour and minute hands. In fact, by making the colour a parameter of drawLine(), you’ve got the method to draw the second hand too.
As for the maths to draw the hands in the right place, it’s trigonometry. Both sin and cos are available in WatchKit, so convert the hours/minutes into radians and calculate the end coordinate of your watch hand, treating the length of the hand as the hypotenuse of a right-angled triangle.
I had a request to post the maths for this. Suppose you want to draw a hand at 12 minutes past. Convert that into a fraction of how far the hand has moved – here, 12 mins out of a possible 60 mins. Then work out whether it’s in the top-right, bottom-right, bottom-left or top-left quadrant of the watch face (that affects which sin/cos functions you’ll use) – here, 12/60 = 0.2, so it’s between 0.0 and 0.25, hence it’s in the first quadrant.
To calculate the x and y coordinates for the top-right quadrant, we form a right-angled triangle between the hand and the y-axis. The hand forms the hypotenuse of the triangle, let’s choose length = 100. So knowing the minutes as a fraction, and the length of the hand, here’s a function to calculate the x and y coordinates as an offset from the centre of the watch face, using trigonometry:
func handCoordinate( _ fraction : CGFloat, length : CGFloat ) -> (x : CGFloat, y : CGFloat )
let x : CGFloat, y : CGFloat
if ( 0 <= fraction && fraction < 0.25 )
let theta = CGFloat( (fraction/0.25) * π / 2.0 )
x = length * sin( theta )
y = -1.0 * length * cos( theta )
// remaining quadrants left as exercise for the reader!
The simplest way of drawing the hand is then a simple drawLine() from (centreX, centreY) to (centreX + x, centreY + y). For my 42mm watch, centreX and centreY are (156,150) – other sizes will vary.
That leaves the trick of how to draw a proper watch hand given only the drawLine() and drawCircle() methods. This is the method I used:
In order to draw the white circle outline with solid black inner-circle, I used this method:
func drawCircle( context : CGContext?, radius : CGFloat, centreX : CGFloat, centreY : CGFloat, colour : CGColor )
let diameter = radius * 2.0
let rect = CGRect( x: centreX - radius, y : centreY - radius, width : diameter, height : diameter )
CGContextSetFillColorWithColor( context, UIColor.blackColor().CGColor )
CGContextFillEllipseInRect( context, rect )
CGContextSetLineWidth( context, 2.0 )
CGContextSetStrokeColorWithColor( context, colour )
CGContextStrokeEllipseInRect( context, rect )
There are a couple of limitations:
That said, I’m really happy with the results:
This is actually one Watch App – I changed the Image to a Button so that I could iterate through different watch faces by tapping the watch. If you do this, call button.setBackgroundImage on the button instead of image.setImage.
See also: How to draw text onto an image and How to draw on top of an image.