By

Deep Dive Into the WatchKit SDK

Starting development on Gero back in February, a productivity companion built for Apple Watch, many aspects of the WatchKit framework were yet to be discovered for the newly-introduced platform. New lessons were learned every day about the platform, and limitations of the device, which lead to tackling these challenges on the fly.

Assuming familiarity with the WatchKit framework, this post will explore some experiences encountered during the development process. Hopefully this post will help developers tackle some of the most common challenges developing a WatchKit App. The most common challenges encountered - and discussed - included controlling the Native App from the WatchKit App, setting up local notifications from the WatchKit App, using custom fonts on the WatchKit App’s Glance View, and caching images. Lastly we’ll explore a small functional programming trick used in our app, Gero, to enable reusability of the code base across both platforms.

Bi-Directional Communication

One of the challenges of designing the Gero app was the need to ensure the Native App always reflected actions taken by the WatchKit App, and vice versa. The standard Apple-defined approach, calling openParentApplication:reply:, was used to communicate from the WatchKit App to the Native App. This made it easy for the Native App to respond both in the foreground and background. Communicating from the Native App to the WatchKit App in real time was tricky. A third party library MMWormhole was used to create a bridge between the WatchKit App and the Native App.

The first step was defining the communication protocol using constants to represent the actions the user can take within the application. Once actions were defined, they were wrapped in a dictionary as the payload communicated from the Native App to the WatchKit App, and vice versa. There were two reasons for wrapping the actions in a dictionary: one was to ensure that we could reuse them as the userInfo for local notifications, and second was to ensure the app listened for a single message identifier implementing the MMWormhole. This behavior will be explored in the latter part of this section.

// Action Definitions

let kNotificationActionStart    = "kNotificationActionStart"
let kNotificationActionPause    = "kNotificationActionPause"
let kNotificationActionResume   = "kNotificationActionResume"
let kNotificationActionStop     = "kNotificationActionStop"

// Dictionaries sent between the Native App to the WatchKit App, and vice versa

let kNotificationValueKey = "notificationType"

let kNotificationUserInfoStart      = [ kNotificationValueKey : kNotificationActionStart]
let kNotificationUserInfoPause      = [ kNotificationValueKey : kNotificationActionPause]
let kNotificationUserInfoResume     = [ kNotificationValueKey : kNotificationActionResume]
let kNotificationUserInfoStop       = [ kNotificationValueKey : kNotificationActionStop]

// Message Identifier for the wormhole
let kWorkholeMessageIdentifier     = "kWorkholeMessageIdentifier"

Communicating from the WatchKit App to the Native App

To relay an action from WatchKit App to the Native App, the standard approach can be used by calling the openParentApplication:reply: method. This is great since the call will open the Native App in the background to perform a background task, and will also be handled accordingly if the app is in the foreground.

// Performed on the WatchKit Extension
func pushWatchKitExtensionRequest(userInfo : NSDictionary){
    WKInterfaceController.openParentApplication(userInfo as [NSObject : AnyObject],
        reply: { [unowned self](reply, error) -> Void in
            // reload interface as needed
        })
}

Once the WatchKit App performs the openParentApplication:reply: method call, the request can be handled in the Native App by overriding the application:handleWatchKitExtensionRequest:reply: method as part of the UIApplicationDelegate. The code sample from the Native App implementation handles the request as such. One can optionally post a notification to default NSNotificationCenter to update the interface if the Native App is in the foreground.

// Perform on the Native Application
func application(application: UIApplication, handleWatchKitExtensionRequest 
    				 userInfo: [NSObject : AnyObject]?, 
    				 reply: (([NSObject : AnyObject]!) -> Void)!) {
    
    var infoDict : NSDictionary = (userInfo as NSDictionary?)!
    let actionString : NSString = infoDict.objectForKey(kNotificationValueKey) as! NSString
   		
   	// Perform logic on the device associated with the action key
   	// Reload interface as needed (Used NotificationCenter to update the interface)
   		
   	reply(nil)
}

Communicating from the Native App to the WatchKit App

The tricky part came in trying to communicate directly from the Native App to the Watchkit App in real time. It was not exactly straight forward at first, but using the open source library MMWormhole allowed the Native App to create a bridge to the WatchKit App.

To implement a MMWormhole, one must first define an instance in both the Native App, and the WatchKit App to create a real time communication bridge. It is very important, when creating a wormhole instance, to pass the shared application group identifier defined in the provisioning profile applicationGroupIdentifier.

// Implemented in both the WatchKit App and Native App
let wormhole : MMWormhole = MMWormhole(applicationGroupIdentifier: kDefaultsAppGroupName,
					        optionalDirectory:kNotificationWorkholeDirectory)

Once the wormhole instances are implemented, the WatchKit App needs to register with the message identifier for which it will be listening. Start listening for the message identifier in willActivate lifecycle call of the WKInterfaceController. Also, don’t forget to unregister during the didDeactivate lifecycle call to stop listening when the WKInterfaceController is inactive.

override func willActivate() {
    super.willActivate()
    
    // Register for Message Identifier
    wormhole.listenForMessageWithIdentifier(kNotificationWorkholeIdentifier, listener: {
        [unowned self] (data) -> Void in
       		let actionString = data.objectForKey(kNotificationValueKey) as! NSString
        	// Perform logic associated with the action key
        })
}

override func didDeactivate() {
    super.didDeactivate()
    
    // Unregister Message Identifier
    wormhole.stopListeningForMessageWithIdentifier(kNotificationWorkholeIdentifier)
}

Recall the reason for wrapping the actions into dictionaries mentioned earlier, and notice that in the example above, one could easily create multiple message listeners for each action. However, using a single message identifier, and parsing the action from the dictionary felt cleaner and allowed for a single communication channel between the WatchKit App and the Native App.

To communicate directly to the WatchKit App, using the Native App’s wormhole instance, we can relay a message directly to the WatchKit App. The snippet below is an example of how send a message in real time.

func startSession() {
	// Perform Native Application Logic, the notify WatchKit App
	notifyWatchKitExtension(kNotificationUserInfoPause)
}   

func notifyWatchKitExtension(userInfo : NSDictionary) {
	// Send a Message to WatchKit App
    wormhole.passMessageObject(userInfo, identifier: kNotificationUpdateInterface)
}

Configure Local Notifications from The Apple Watch

Architecturally developing the Gero app, the intent was to use a single controller for all the timing logic within the application. Initially the logic implementation to set up local notifications was within this controller, but when shared between the WatchKit App and Native App’s targets, it was quickly realized that one cannot access the sharedApplication instance of UIApplication to setup these local notifications.

Though the WatchKit App has access to the Foundation Library to create instances of UILocalNotification, it is sandboxed in its own environment, and does not have access to the Native App’s shared instance of UIApplication, which is responsible for setting up local notifications. While keeping the logic for generating the local notifications within the shared controller, a quick little extension was implemented to set up up local notifications for handling WatchKit App’s request in the application:handleWatchKitExtensionRequest:reply: UIApplicationDelegate method call.

import Foundation
extension UIApplication {

	func setupLocalNotifications(notifications : NSArray) {
  		clearLocalNotifications()
    
    	var notificationsCopy : NSArray  = notifications.copy() as! NSArray
            
    	for localNotification in notificationsCopy as [AnyObject] {
        	UIApplication.sharedApplication().scheduleLocalNotification(localNotification as! UILocalNotification)
    	}
	}

	func clearLocalNotifications() {
   		UIApplication.sharedApplication().cancelAllLocalNotifications()
	}
}

From an architectural perspective, this isolated all of the core logic into the shared logic controller to generate the local notifications. This easily allowed the setting, or clearing, of the notifications when the WatchKit app notified the Native Application using the openParentApplication:reply:method described in the previous section.

Creating Custom Animations

The WatchKit Framework does not support Core Animation, yet Apple provided a way to create animations using image sequences. The animated images use a convention <name><number>.<extension>, where the <name> and <extension> strings are the same for all images and the <number> value indicates the position of the image in the animation sequence. Once all the animation frames are set up, one must be sure to package it as part of the WatchKit App’s bundle. This ensures that they do not need to be uploaded to the WatchKit App when they need to be displayed.

Animating an Image

To animate an image, call startAnimatingWithImagesInRange(imageRange:duration:repeatCount:) method on the WKInterfaceImage, or startAnimatingWithImagesInRange()imageRange: duration:repeatCount:) on an WKInterfaceGroup. Since the WatchKit Framework does not allow layering multiple interface objects one on top of the other, it can be handy to use an WKInterfaceGroup’s background image to create animation behind WKInterfaceGroup’s contents.

// Starting Animation for a WKInterfaceImage
animatedImage.setImageNamed("activeframe")
animatedImage.startAnimatingWithImagesInRange(NSRange(location : 1, length : 30, duration: 60.0, repeatCount:1)

// Starting Animation for a WKInterfaceGroup
imageGroup.setBackgroundImageNamed("activeframe")
imageGroup.startAnimatingWithImagesInRange(NSRange(location : 1, length : 30, duration: 60.0, repeatCount:1)

One detail to notice here is the repeat count: When set to 1, the animated image will stop after it reaches the end of the animation of a single cycle. If we set it to 0, it will restart the animation indefinitely upon completion. This detail felt a little unintuitive at first, but is consistent with Apple’s approach if recalling that setting the numberOfLines property on a UILabel implies an unlimited number of lines when autoresizing.

Efficiently Creating Animation Frames

When designing the circle indicator for the Gero app, at first all the images were done one at a time by activating a single circle, then saving the file manually while updating the name for every single single frame. This turned out to be extremely inefficient, since there were 120 frames total: 30 yellow and blue frames for the WatchKit App, and 30 yellow and blue frames for the WatchKit App’s Glance View with a different alignment.

This process can become time-consuming when having to recreate images, especially to adjust the color palette, or alignment of the images. After discussing the inefficiency issue with the designers, a solution surfaced to create layer comps then export them using scripts packaged with Photoshop. This can be done by going to File -> Scripts -> Layer Comps to Files Option.

The scripted approach bypassed the issue of manually setting up the indicator layers, and exporting them one by one.

Yet another issue surfaced that the pre-packaged script in Photoshop inserted a numeric prefix with the following pattern XXXX for every single file it exported. This meant that after exporting the files, someone still needs rename all the files in the export directory. After researching online, a blog post by designer James Tenniswood provided the solution to this with an updated script for Photoshop which magically excluded the prefix on import :)

To install the script, navigate to Photoshop’s application folder -> Presets -> Scripts, and then replace the Layer Comps To Files.jsx file with the following: script. Now Photoshop will generate frames efficiently and with flexibility to adjust images on the fly as efficiently as possible.

Custom Fonts on Glances

Interface Builder in Xcode will complain when trying to set a custom font on the Glance View. We quickly realized that custom fonts within the glance view are not supported. This limitation was not accounted for during the design process, yet it was a big part for the application’s user experience.

The approach to solve this was to incorporate core graphics to render a bitmap for the countdown timer using custom fonts, then transfer the image to the watch for display.

Generating Images Using Core Graphics

The snippet below is meant as a simple quickstart of how you could use core graphics to generate custom images.

func generateGlanceCountDownImage(countString : NSString, timerSubstring : NSString, frame : CGRect) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(frame.size, false, 3.0);
    
    let context = UIGraphicsGetCurrentContext()
    CGContextSetInterpolationQuality(context, kCGInterpolationHigh);

    CGContextSaveGState(context)
	
	// Core Graphics Drawing Code Goes Here
	
    CGContextRestoreGState(context)

    var outputImage = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();
    return outputImage
}

One of the best tools for generating core graphics code is Paintcode. The upgraded version can import a designer’s SVG, or PSD, and instantly turn it into dynamically resizable code which generates an image for display. More documentation can be found on Paintcode’s Documentation Site

Now that we have a general starting point as to how to generate images using core graphics, in the next section, let’s explore how we can minimize the overhead in the next section by caching the images transferred to the device.

Caching Transferred Images

Generating images with core graphics can be heavy, and transferring them to the WatchKit App adds to the cost in terms of processing power and battery life. Unlike the indicator in the Gero app, which is intended to be modified between releases, the countdown timer may change in case there is a change to the time intervals in the future. To ensure that this is dynamic, it is essential to cache the images after they are received on the device for the first time.

The code snippet below is an example of caching images on the fly. Once the setCountDownImage(timeRemaining:timerSubString) method is called, it generates a unique image key, then checks if the image is already cached. If it is determined that the image is not cached, and image is generated using coregraphics, then set using the setImage: or setBackgroundImage: methods accordingly the first time around. This is due to the fact that the image is not available from the cache immediately. The second time around, if it is determined that the image is cached, the setImageNamed: or setBackgroundImageNamed: methods can be used to set the image directly from the WKInterfaceDevice cache.

func setCountDownImage(timeRemaining : String, timerSubString : String) {
   
    var imageKey = timeRemaining + timerSubString
    var cachedImageDictionary = WKInterfaceDevice.currentDevice().cachedImages as NSDictionary

	// Check to see if the image is cached
    if (cachedImageDictionary.objectForKey(imageKey) == nil) {
    	
    	// Generate the image
        let image = generateGlanceCountDownImage(timeRemaining, substring : timeSubString, 
        frame : contentImage.frame)
        
        // Cache the for the imageKey
        WKInterfaceDevice.currentDevice().addCachedImage(image, name: imageKey)
      	
      	// Set the image returned
        contentImage.setImage(image)
    } else {
        contentImage.setImageNamed(imageKey)
    }
}

Trick for Code Reuse

As mentioned in the section about local notification configuration in this article, the intent architecturally was to reuse a single controller for all the timing logic, and reuse it in the WatchKit App and the Native App. The secretary pattern using a delegate could very much have been used here, and been fairly efficient, yet reimplementing the delegate methods methods in multiple places could have gotten messy fast.

A quick functional trick used in the Gero app, was to declare a closure as a variable in the logic controller which would be called when data changed, updating the interface as needed.

// Declare a variable to store the closure in the logic controller
public var interfaceUpdateBlock:()->Void = {} {
     didSet {
        self.updateTimer()
        interfaceUpdateBlock()
    }
} 

Next, we defined a closure in the view controller of the Native App, WatchKit App, or WatchKit App’s Glance View to handle the events accordingly.

lazy var interfaceUpdateBlock : () -> Void = {
    [unowned self]  in
    
   // Update Interface Accodingly
}

Upon initialization, set interface update closure on the logic controller during awakeWithContext(context: AnyObject?) in WKInterfaceController, or during viewDidLoad in the ViewController to handle the interface update event accordingly.

// WKInterfaceController 
override func awakeWithContext(context: AnyObject?) {

	super.awakeWithContext(context)
	sprintManager.interfaceUpdateBlock = self.interfaceUpdateBlock
}

// ViewController 
override func viewDidLoad() {
    super.viewDidLoad()
    sprintManager.interfaceUpdateBlock = self.interfaceUpdateBlock
}

Further thoughts?

We hope you enjoyed the read, and that this post gives developers a good jumping-off point to expedite their development process when delving into the world of the WatchKit Apps for the first time. Gero is available now for your iPhone and Apple Watch. If there is anything we missed, have feedback, or questions, do not hesitate to contact us!

Written by
Senior iOS Developer with a pure passion for the little details