Tag Archives: WatchKit

How to use the Digital Crown in a Watch Face App

I’ve written before that, having lived with the Apple Watch for a while, I felt the watch faces lacked variety. So, I wrote a watch face app, which I now use for the majority of the time.

For my next watch face, I thought it would be pretty cool to recreate the look of my beloved Tissot watch. One of its best features is that extra functionality is controlled by twisting/pressing the bezel. It supports date/time/stopwatch/timer – or you can hide the digital display altogether. Although WatchKit doesn’t allow you to capture a button press, you can use the digital crown to scroll between choices – so I thought I’d offer a couple of date formats as well as the option to hide the date altogether. tissot

Add a Picker control onto your storyboard and connect it to a WKInterfacePicker in your InterfaceController. There are a number of styles that you can choose, but for my purposes, I chose the default List style:
screen-shot-2017-01-12-at-19-09-55
Then it’s a matter of populating the picker’s list of items – for example, this code could go in function awake(withContext):

            let blankLine = WKPickerItem()
            blankLine.title = ""
            
            let currentDate = Date()
            let dayDateMonthLine = WKPickerItem()
            dayDateMonthLine.title = "\(currentDate.dayOfWeek()) \(currentDate.dayOfMonth()) \(currentDate.monthAsString())"
            let dateMonthYearLine = WKPickerItem()
            dateMonthYearLine.title = "\(currentDate.dayOfMonth()) \(currentDate.monthAsString()) \(currentDate.year())"
            
            InfoPicker.setHidden( false )
            InfoPicker.setItems( [ blankLine, dayDateMonthLine, dateMonthYearLine, blankLine ] )
            InfoPicker.focus()

I set the focus on the picker so that the digital crown is immediately responsive (otherwise, you’d have to select the picker first on the touch screen). I also put a blank line before and after the date formats, to make the usage more natural. The code above also uses some extension methods on Date.

Tissot style watch face

Seeing how good this looks, I think Apple are missing a trick. All of their analogue Apple Watch faces are round – I hope they ship a couple of rectangular faces in the next Watch OS upgrade.

2 Comments

Filed under Programming, Swift

How to draw on top of an image in Apple WatchKit

Suppose you’re writing a simple game for Apple Watch – for example, you might have a treasure map image and you want to render a cross on it in a random position to locate the treasure.
treasure-map
This is tricky, because WatchKit severely limits your options for laying out UI primitives on the screen. For example, if you put a label and an image onto a StoryBoard, it will tile them (rather than letting you put one on top of the other).

The approach I’ve adopted is:

  • Create a Group in the story board and set its background image
  • Add an image view within the Group
  • Create a context and use CoreGraphics to write into it
  • Apply the context to the image view

Set up a new iOS WatchKit App, then drag a Group and Image from the Object Library onto the storyboard:
TreasureMap StoryBoard

In the WatchKit App assets, create a new image set and drag your background image onto the x2 outline:
TreasureMap ImageSet

Set the background image on the group:
TreasureMap SetBackground

Then create an outlet in the InterfaceController for the image – one way is to control-drag from the outline view of the storyboard into the interface controller’s swift file. I called mine OverlayImage to convey the purpose.

Finally, add the code that will leverage the CoreGraphics library to draw into the overlay – the work is done in drawCross() which is called from awakeWithContext(). I’ve split out line and circle drawing methods for clarity.

class InterfaceController: WKInterfaceController {
    // Create by control-dragging to the StoryBoard
    @IBOutlet var OverlayImage: WKInterfaceImage!
    
    let imageWidth : CGFloat = 312.0
    let imageHeight : CGFloat = 348.0 // 390 - 42 for status bar on 42mm watch
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        drawCross()
    }

    override func willActivate() { ... } // standard
    override func didDeactivate() { ... } // standard
    
    func drawCross()
    {
        // Begin image context and grab context
        let context = createContext()
        
        // Draw our primitives
        drawLine( context, startX: 75, startY: 150, endX: 125, endY: 200 )
        drawLine( context, startX: 75, startY: 200, endX: 125, endY: 150 )
        drawCircle( context, radius : 10, centreX : 100, centreY : 175 )
        
        // End by applying our graphics to the Overlay image
        applyContextToImage( context )
    }
    
    func createContext() -> CGContext?
    {
        // The 'opaque' parameter is false, so that we overlay 
        // rather than the static image underneath
        UIGraphicsBeginImageContextWithOptions( CGSizeMake( imageWidth, imageHeight ), false, 0 )
        let context = UIGraphicsGetCurrentContext()
        CGContextBeginPath( context )
        
        return context
    }
    
    func drawLine( context : CGContext?, startX : CGFloat, startY : CGFloat, endX : CGFloat, endY : CGFloat )
    {
        CGContextSetStrokeColorWithColor( context, UIColor.blackColor().CGColor )
        CGContextSetLineWidth(context, 3.0)
        CGContextMoveToPoint( context, startX, startY )
        CGContextAddLineToPoint( context, endX, endY )
        CGContextStrokePath( context )
    }
    
    func drawCircle( context : CGContext?, radius : CGFloat, centreX : CGFloat, centreY : CGFloat )
    {
        let diameter = radius * 2.0
        let rect = CGRect( x: centreX - radius, y : centreY - radius, width : diameter, height : diameter )
        
        CGContextSetLineWidth( context, 3.0 )
        CGContextSetStrokeColorWithColor( context, UIColor.blackColor().CGColor )
        CGContextStrokeEllipseInRect( context, rect )
    }
    
    func applyContextToImage( context : CGContext? )
    {
        let img = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        OverlayImage.setImage( img )
    }

All being well, you can now run your WatchKit App and check that the black cross and circle have been drawn on top of the background image!

TreasureMap WatchApp

3 Comments

Filed under Programming, Swift

Calculator App for Apple Watch

I was surprised that Apple didn’t include a calculator app with the Apple Watch. In the 1980’s, calculator watches were cool – having missed out then, I was keen for my Apple watch to have one.CasioCalculatorWatch

I chose to write my own calculator WatchKit App for fun, rather than purchasing one from the App Store. It’s a good choice for a first project, because the user interface is static and the focus is more on getting up the learning curve of WatchKit development. Here are some of the lessons I learnt in the process.

How to get the text from a WatchKit label
Suppose you’ve set up an outlet for a label that displays the input figures in your app – then you’d expect to be able to get the text back from it:

@IBOutlet var labelSum: WKInterfaceLabel!
//...
labelSum.setText( "42.0" )
let digits = labelSum.getText() // error - does not compile

It seems this is not supported in Xcode 7.2/Watch OS 2.1. Instead, you have to track the state in a variable and use that to populate the label.
How to store state in a WatchKit App
For a calculator App at least, you need a state machine to keep track of state, because at different stages you may be inputting the first or second number in the calculation, or re-using the previous answer in another calculation. Swift enum is a discriminated union that is well suited to this:

// Define enum type outside your interface controller
enum CalculationState
{
    case BuildingLHS( String )
    case BuildingRHS( LHS : String, Op : Operation, RHS : String ) // Waiting for Equals
    case WaitingForOperation( String ) // Got answer already, may do another operation
}

// Declare a variable to hold the state 
// as a member inside the interface controller class
class InterfaceController: WKInterfaceController {
    // ...
    var state : CalculationState
}

Use ‘RelativeToContainer’ to scale layout
When defining the UI elements on your story board, the interface controller is only approximately the size of the watch screen. My UI has all the buttons displayed at once, so it’s important to make maximum use of the screen size.
Calc - RelativeToContainer
Here, buttons 7, 8 and 9 are in a horizontal group. To fill that container, we need to use the ‘RelativeToContainer’ scaling style and fill in the proportion each UI element should take. For height, it’s 1 (i.e. the whole container), whereas for width, it’s one third (i.e. 0.33-ish). Personally, I think it would have been more obvious that this is the scaling style to choose if the proportion was displayed as a percentage, rather than a value relative to one.
How to set completion after animation
The WatchKit animation API lacks the ability to specify a completion function that runs after the initial animation changes. This is awkward if you want to flash a UI element from one colour and return to the original colour – if you run both animations in parallel, the colour doesn’t change. I used this code to add an extension method – then I could easily flash UI elements as below:

    func flashSum( origColor : UIColor, flashColor : UIColor ){
        animateWithDuration(0.25,
            animations: { () -> Void in self.labelSum.setTextColor( flashColor ) },
            completion: { () -> Void in self.labelSum.setTextColor( origColor )}
        )
    }

How to run WatchKit App on hardwareThis should be as simple as plugging the hardware into your MacBook (i.e. the iPhone that’s paired to the Apple Watch), selecting the correct device from the devices list, then running the App in the debugger. However, there are numerous pitfalls:

  • You need either a developer licence or a personal team set up. See Team | Identity in the Project settings
  • Xcode may think that the iPhone and Watch are unpaired – restarting Xcode solved this one for me, other people have had to re-boot their watch and/or phone
  • Xcode may not have the symbols for your Watch OS (this happened after I updated to Watch OS 2.1) – however, it seems happy to download them once you connect to the internet

Re-booting, re-connecting the phone to the MacBook, re-starting Xcode eventually sorted this out.
Conclusion
I’d already worked through a couple of tutorials for writing WatchKit apps, but you learn far more by writing your own.
Calc On Watch
The end result looks great, although the buttons are slightly too small for frequent use, even on a 42mm Apple Watch.

1 Comment

Filed under Programming, Swift