Writing a WatchKit Complication in watchOS 2

One of the exciting new additions to the WatchKit Framework in watchOS 2 is the ability to add custom complications to the clock faces provided by Apple. We've written a quick guide on how to add custom Complications to your watch app.

Implement CLKComplicationDataSource

All of the magic happens in CLKComplicationDataSource. Create a new class on your WatchKit Extension target that implements this delegate. Since every delegate method is required, we can start by adding the skeleton of every method in the delegate.

import ClockKit

class Cowmplication: NSObject, CLKComplicationDataSource {
    
    func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
        handler(nil)      
    }

    func getPlaceholderTemplateForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTemplate?) -> Void) {
        handler(nil)
    }
    
    func getPrivacyBehaviorForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {
        handler(CLKComplicationPrivacyBehavior.ShowOnLockScreen)
    }
    
    func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimelineEntry?) -> Void) {
        handler(nil)
    }
    
    func getTimelineEntriesForComplication(complication: CLKComplication, beforeDate date: NSDate, limit: Int, withHandler handler: ([CLKComplicationTimelineEntry]?) -> Void) {
        handler(nil)
    }
  
    func getTimelineEntriesForComplication(complication: CLKComplication, afterDate date: NSDate, limit: Int, withHandler handler: ([CLKComplicationTimelineEntry]?) -> Void) {
        handler([])
    }

    func getSupportedTimeTravelDirectionsForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {
        handler([CLKComplicationTimeTravelDirections.None])        
    }
    
    func getTimelineStartDateForComplication(complication: CLKComplication, withHandler handler: (NSDate?) -> Void) {
        handler(NSDate())
    }
    
    func getTimelineEndDateForComplication(complication: CLKComplication, withHandler handler: (NSDate?) -> Void) {
        handler(NSDate())
    }
}

You never need to create an instance of this class, and Apple will handle instantiating it using the default constructor.

Understanding Complication Families

There are 5 families of complications that we need to become familiar with in the CLKComplicationFamily enum. From left to right, here are images of ModularSmall, ModularLarge, UtilitarianSmall, UtilitarianLarge, CircularSmall.

From within these families, to populate data, you implement different complication templates related to each family. For example, CLKComplicationFamily.CircularSmall can use the following templates:

  • CLKComplicationTemplateCircularSmallRingText
  • CLKComplicationTemplateCircularSmallRingImage
  • CLKComplicationTemplateCircularSmallStackText
  • CLKComplicationTemplateCircularSmallStackImage

There are many templates available. Take a look at the list of subclasses to CLKComplicationTemplate in the ClockKit Framework Reference to see all of the options. Clicking in on a template you can see Apple's visual diagram of how the information in each template is presented.

Configure Info.plist

Go to your targets and select your WatchKit Extension target. Under the General tab, set the Data Source Class to the class delegate we created above prefixed with $(PRODUCT_MODULE_NAME). For example, since our example class was Cowmplication, we put $(PRODUCT_MODULE_NAME).Cowmplication for Data Source Class.

Next, check off which complication families you want to support. Most likely you'd want to support all families, but we're just going to implement CircularSmall for this example.

Set Privacy Behavior

You can choose to show or hide your complication data if the watch is locked, especially if you are displaying more private or sensitive information by passing either CLKComplicationPrivacyBehavior.ShowOnLockScreen or CLKComplicationPrivacyBehavior.HideOnLockScreen to the handler of getPrivacyBehaviorForComplication().

    func getPrivacyBehaviorForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {
        handler(CLKComplicationPrivacyBehavior.ShowOnLockScreen)
    }

Set Refresh Frequency

Implement getNextRequestedUpdateDateWithHandler() delegate method to tell the watch how often to refresh the complication data. Apple recommends choosing hourly or even an entire day, and providing as much information as possible in a single update cycle with your complication. This will avoid unnecessary battery life drains.

The API gives you a handler you need to call, passing in the date of the next update.

    func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
        // Update hourly
        handler(NSDate(timeIntervalSinceNow: 60*60))
    }

Implement Placeholder Templates

If data is not populated, especially when customizing the clock face, the OS will show a placeholder for your complication.

You set this up with the getPlaceholderTemplateForComplication() delegate method. It's important to know that this method is called only once, during the installation of your app and the placeholder is cached, so you won't be able to customize this later on.

    func getPlaceholderTemplateForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTemplate?) -> Void) {
        var template: CLKComplicationTemplate? = nil
        switch complication.family {
        case .ModularSmall:
            template = nil
        case .ModularLarge:
            template = nil
        case .UtilitarianSmall:
            template = nil
        case .UtilitarianLarge:
            template = nil
        case .CircularSmall:
            let modularTemplate = CLKComplicationTemplateCircularSmallRingText()
            modularTemplate.textProvider = CLKSimpleTextProvider(text: "--")
            modularTemplate.fillFraction = 0.7
            modularTemplate.ringStyle = CLKComplicationRingStyle.Closed
            template = modularTemplate
        }
        handler(template)
    }

In this example code, we've only implemented the delegate method for .CircularSmall, but in your apps, you'd likely want to configure the look for most or all of the complication types.

For the placeholder, you are not supposed to populate with example data, which is why we put "--" for the text, following what Apple's stock complications do when they put their own placeholders.

Populate Your Complication With Real Data

Next, we'll actually implement the delegate method that will provide real data for the complication. Again, while in the code snippet we only implement .CircularSmall, you would build multiple templates for all of the different complication families you support.

    func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimelineEntry?) -> Void) {

        if complication.family == .CircularSmall {
            let template = CLKComplicationTemplateCircularSmallRingText()
            template.textProvider = CLKSimpleTextProvider(text: "\(getCurrentHealth())")
            template.fillFraction = Float(getCurrentHealth()) / 10.0
            template.ringStyle = CLKComplicationRingStyle.Closed

            let timelineEntry = CLKComplicationTimelineEntry(date: NSDate(), complicationTemplate: template)
            handler(timelineEntry)
        } else {
            handler(nil)
        }
    }

This introduces the concept of a CLKComplicationTimelineEntry, which is a container that pairs a date with a complication template. getCurrentTimelineEntryForComplication is used to populate the current complication data, which is why we use NSDate() to indicate the current date and time.

Refreshing The Complication From Your App

It's a very common scenario that while the user is using your app, you'll want to update your complication as you know the data is stale or incorrect. You can use the CLKComplicationServer singleton to trigger updates to your complications.

        let complicationServer = CLKComplicationServer.sharedInstance()
        for complication in complicationServer.activeComplications {
            complicationServer.reloadTimelineForComplication(complication)
        }

Apple has indicated that reloadTimelineForComplication() is rate limited to preserve battery life. If a complication exceeds a daily limit, it will ignore calls to refresh for the remainder of that day.

Build, Run & Test

 The completed Complication.

The completed Complication.

If you didn't make any typos, you should be able to now test & run this on the simulator or on a device. If you are having trouble finding it, make sure that you are looking for it on the right clock face, since not all clock faces support all families of complications.

Time Travel

Improving the code to implement Time Travel is relatively straightforward. First, update getSupportedTimeTravelDirectionsForComplication() to indicate if your complication supports values into the future or the past. For example, a stocks complication would only make sense to show values in the past, while a weather complication could show values in the past and the future.

    func getSupportedTimeTravelDirectionsForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {
        handler([.Backward, .Forward])
    }

Next, implement getTimelineEntriesForComplication(complication:beforeDate:limit:withHandler:) and getTimelineEntriesForComplication(complication:afterDate:limit:withHandler:) which is similar to getCurrentTimelineEntryForComplication() except that you pass in an array of CLKComplicationTimelineEntry objects. Don't return more objects than limit, and make sure your dates are in sequential order and all occur before or after the passed in dates.

Finally, implement getTimelineStartDateForComplication() and getTimelineEndDateForComplication() to indicate the ranges of time travel you support.

Wrapping Up

You can check out the completed project code on our Sneaky Crab GitHub so you can quickly get going.

Apple has provided a very simple and powerful API to convey simple, glance-able information right on the clock face for app developers.

Tell us about the cool complications that you come up with in the comment section below!

Update 6/22/15: watchOS 2 beta 2 bugs

If you're having trouble with Complications when using beta 2, be aware of the known issues and workarounds from the watchOS 2 Release Notes from Apple.

Known Issues

  • Complications are disabled across launches of Simulator.
    Workaround: After enabling a complication in the Watch Simulator you need to lock the watch sim, using Sim Menu > Hardware > Lock (or Command-L), to have the complication still be enabled after quitting Simulator and relaunching.

  • Location request dialog text is jumbled in Simulator.

  • CLKImageProvider objects do not currently honor the foregroundImage property.
  • The CLKComplicationRingStyle property is currently not honored on any CLKComplicationTemplate.
  • The CLKRelativeDateStyleOffset enumeration of CLKRelativeDateStyle is not honored for use in CLKRelativeDateTextProvider: It appears as CLKRelativeDateStyleNatural.
  • The CLKComplicationPrivacyBehavior on CLKComplicationDataSource is not currently honored.

watchOS 2.0 tl;dr

Apple announced a major update to watchOS 2 today that will be available with iOS 9 this fall. We've gone through the official Apple documentation, all of the headers and code provided in Xcode 7, and summarized everything for you so you can quickly skim what's coming up.

The WatchKit Extension now runs on the Apple Watch

In watchOS 1, the WatchKit extension ran as an extension of your iOS App. In watchOS 2, it runs natively on the Apple Watch. This is great because you get native performance without the slow back-and-forth communication over Bluetooth for every UI interaction.

There is a downside though–current WatchKit extensions enjoy the use of every iOS API, whereas the new watch OS 2 apps have access to a subset of APIs available only to WatchKit. For example, if your extension depended on CloudKit, you can't use it directly on the Watch and will have to contact the parent iOS app if you want to continue using that functionality.

Watch Connectivity is for synchronizing the iPhone and Watch

Now that the watch app runs on the actual watch device, synchronizing data and state between the iPhone and watch is a little trickier. Apple has added Watch Connectivity with the WCSession class to facilitate communication between the two devices.

You bundle watchOS 1 & 2 apps in the same app

Apple will allow you to continue to bundle the old WatchKit extension code for watchOS 1 users in addition to shipping new code for watchOS 2. If you do this, sharing code might be tricky as different APIs are available between the two and it doesn't appear as though embedded frameworks will work on the watch.

WKExtensionDelegate is the AppDelegate for WatchKit

One of the annoying things in watchOS 1 was how difficult it was to track app lifecycle changes. Now you simply implement WKExtensionDelegate similar to how you implement AppDelegate on iOS. There's a simplified lifecycle, with three states: Not Running, Inactive and Active.

You can add Complications for your app to the watch face

If you implement CLKComplicationDataSource from ClockKit, you can set up complication information. You provide a timeline, where you describe the various changes to your complication throughout the day, as well as an interval for when ClockKit should refresh your complication. There is also a high-priority push-notification that you can use meant for updating the Complication in real time.

Check out our guide on Writing a WatchKit Complication.

You can play Video and Audio content on the watch

For embedded video and audio in your UI, use WKInterfaceMovie. To present this in a new controller, use presentMediaPlayerControllerWithURL:options:completion:. If you want to play extended content that continues playing even when your app is inactive, use WKAudioFilePlayer and set up the audio value in UIBackgroundModes in your Info.plist.

Audio plays directly on the watch, prioritizing the Bluetooth speaker, but will also use the built-in speaker if a Bluetooth speaker is not available.

You can record audio with the microphone

It seems you can only do it through Apple's stock audio recording controller, calling presentAudioRecordingControllerWithOutputURL:preset:maximumDuration:actionTitle:completion:. There doesn't seem to be a way to record audio and process realtime audio or record audio inline or using a custom controller or UI.

You can use the Taptic Engine to provide haptic feedback

WKInterfaceDevice.playHaptic() will provide haptic feedback. You can pick from one of the choices in the WKHapticType enum. We didn't find a way in the documentation or header files to define custom haptic feedback.

Check out our guide on Writing Haptic feedback on the Taptic Engine.

Digital Crown Access

The WKInterfacePicker handles digital crown access. You can have a List UI, a stack of cards that animate, or an image sequence that responds to the digital crown. We didn't find a way to read raw delta values from the crown in the current API, which may be useful for game developers.

Check out our guide on coding your own WKInterfacePicker: Using the Digital Crown.

Access to Sensors

Opening up Xcode 7, we were able to import CoreMotion and instantiate a CMMotionManager in a test WatchKit Extension project. This appears to work exactly like in iOS. We assume that heart rate and step data are also available through the HealthKit framework.

HealthKit, HomeKit, PassKit, CoreLocation, CoreGraphics

In Xcode 7, we were able to import and instantiate objects from all of these libraries from within our WatchKit extension project, so we assume that these work just as they do from iOS. CoreAnimation did not appear to be available.

Xcode Simulator

The Xcode simulator now fully supports the watch. This means that you can test glances, notifications, watch complications and so on. You can also debug and run the watch and device simulators simultaneously which was not possible in watchOS 1 simulators.

In addition, the Hardware menu has added "Simulate Touch Pressure" with an option for Shallow Press and Deep Press. Scrolling your mouse-wheel simulates digital crown movement, and pressing ⌘⇧H simulates pressing the crown in.

To be continued...

As WWDC unfolds, we'll update the information here with corrections and the latest information as Apple provides it. All in all, watchOS 2 is an extremely exciting improvement for developers, and we can't wait to update Chairman Cow to the new features, and stay tuned as we are determined to bring you world class watchOS 2 apps and games this fall!

Edit: 6/8/15 Added information about recording audio/microphone.

Speeding up slow app install times when debugging on a real Apple Watch

Fast Compile ➡ Install ➡ Run times are essential for developer productivity, especially when learning a new SDK. Given how new WatchKit is and the sparse detail of the official documentation, the only real way to really understand the call patterns and behaviors of WatchOS and WatchKit is to write lots of sample code and trace or debug it.

One of the most maddening things is how long it takes to install an app to a real Watch. This was particularly frustrating lately for two reasons:

  1. The simulator, while helpful, sometimes has different bugs or behaviors from the Watch itself.
  2. Apple has not released an updated version of Xcode with a WatchOS 1.0.1 simulator, so testing new behaviors such as the change to WKInterfaceController didAppear() caching requires testing on a real Watch.

Let's take a journey to find the fastest way to get from building to having a debugger attached to the Watch app and save all of our sanity.

The most expensive part is installation, so we profiled modifying a few variables to see what made the biggest impact for install time of our app. We tested installing the app over Bluetooth and also forced app install over Wi-Fi, and also tried to see if having the Apple Watch App open, on the app detail and not asleep mattered. This was tested on Xcode 6.3.2, iOS 8.2 and WatchOS 1.0.1.

Time Wireless Apple Watch App
4:23 Bluetooth Background
3:37 Bluetooth Background
2:39 Bluetooth Background
2:00 Bluetooth Background
1:26 Bluetooth Foreground
1:12 Bluetooth Foreground
1:06 Wi-Fi Background
1:01 Wi-Fi Foreground
1:00 Wi-Fi Foreground
1:00 Wi-Fi Background

Bluetooth installs were faster if the Apple Watch App was in the foreground.

From our admittedly unscientific tests, we can draw two conclusions:

  1. Bluetooth installs seem to be significantly faster if the Apple Watch App is in the foreground
  2. Wi-Fi was fast in every test.

Due to the consistency of the Wi-Fi approach, I'll focus on that here.

Setup

Make sure that your Apple Watch is set up to use Wi-Fi.

  • The Apple Watch can only use 2.4 Ghz Wi-Fi networks, so if your Wi-Fi is 5 Ghz and you can't enable 2.4 Ghz, you're out of luck.
  • The phone will automatically share Wi-Fi credentials with your watch.
  • You can test if it is working by turning off Bluetooth on your phone, and seeing if you can still use Siri or make a phone call with your watch.

Make sure your Apple Watch is paired with your iPhone, and it's identifier has been added to your Devices section in Certificates, Identifiers & Profiles in developer.apple.com.

Fastest Build -> Install -> Run -> Debug Cycle

  1. Turn off Bluetooth on your phone. The fastest way is to swipe up control center and toggle it there. You'll get a dialog nagging you to turn it back onhit cancel. We do this first to make sure that the system doesn't accidentally start installing the app over Bluetooth. If you turn off Bluetooth after it began installing over Bluetooth, we've found it hard to convince the phone to continue installing over Wi-Fi. It seems to prefer the network type it began the install.
  2. Hit stop in Xcode until it greys out. We've had lots of problems with Xcode attached to different old processes that cause problems later on. We recommend ensuring you're in a clean state.
  3. Choose the iOS base scheme in Xcode, and your device. If you build and run the WatchKit Extension scheme here, Xcode will time out trying to attach while you're waiting for it to install on the Watch which is more dialogs for you to click on.
  4. Click the Xcode play button to begin build and installation to your phone.
  5. When the iOS App starts running, multitask back to the Apple Watch App, open the page for your App. While this doesn't seem to speed up Wi-Fi installs, the system seems to notice faster that it needs to install an updated watch app than when you aren't on this screen. Obviously, make sure Show App on Apple Watch is enabled.
  6. Your app should install to device in about 1 minute.
  7. Turn on Bluetooth. You can't launch or debug your app over Wi-Fi, so this must be done. Again, swiping up control center is the fastest way to toggle Bluetooth.
  8. Back in Xcode, press ⌘6 to open the Debug Navigator  
  9. Hit stop again, switch to the WatchKit Extension Scheme in Xcode. Now we're going to attach debugging.
  10. Hit play in Xcode, when it is ready, you'll see in Debug Navigator show the extension as "Waiting to Attach".
  11. Manually open the app on your watch, and you'll see Xcode magically attach to the process.

Phew. That's a lot of steps. But we got pretty fast at it while we were trying to debug issues in our Chairman Cow game with WatchOS 1.0.1, and it's better than waiting 4 minutes per build for the app to install.

This is the best method we found. What are your tips and tricks for efficient Build ➡ Run ➡ Debug on a real watch? Please tell us if you have found a better way!