Building WatchKit Apps in Swift

The Apple Watch is coming this spring, and WatchKit is the SDK that allows us to build apps for it. In this talk, developer Ben Morrow reviews the features and limitations of the SDK, and uses Swift to code a simple “Hello World” Watch app with Xcode. The walkthrough will include how to add a Watch App target, how to interact with your app in the simulator, and more! The code used in this video can be found on GitHub.


Apple Watch Timeline Overview (1:49)

Around the end of November, Apple finally released WatchKit. That same weekend, we had a hackathon with some really neat ideas, with event videos and descriptions found here. Apple is currently working on iOS 8.2, which should enable the functionality of the phone talking to the watch. Then there’s the possibility that the watch will come out in March and developers will be able to release our apps on the store.

Before WatchKit was announced, we weren’t sure what we sould get. We thought we might just get notifications, but we got much more than that. There’s the concept of split apps, where you have the interactive user interface on the watch and the logic in the iOS app on your phone. Views are actually called interfaces, and so in a storyboard you set up your interfaces, which will need a controller to populate said interface with data. The two devices communicate over Bluetooth. You can share data through a shared app group, which is a new concept unveiled recently in iOS.

There are also glances, where you swipe up from the bottom of the watch display to see single page views of the data from the apps. If you tap on one of the glances, it’ll launch the watch app. Notifications are also better than expected, because you can provide custom action so that different kinds of buttons in the notifications can do different things.

Classes & methods (5:58)

With WatchKit, there’s a whole new set of code and classes that we have access to. WKInterfaceController is the replacement on the watch for UIViewController, which will be used to populate the interfaces with data. A WKInterfaceObject is something like a button or a switch. They’re really proxy objects because you can’t write to them directly, but you can request that things are sent to them. WKInterfaceDevice will tell you about the resolution or locale of your device.

There are also some methods you’ll see as you’re programming for WatchKit. To move between different interfaces, use segues in your storyboards. Then you can provide a context that can be a string or a data object. When you move to that new interface, awakeWithContext is called and can be used to fill out the data on the screen. There are also methods to change different attributes for different objects, including ‘setText:’, ‘setHidden:’, ‘setAlpha:’, and ‘setWidth:’. Finally, the function ‘openParentApplication(_:reply:)’ lets you communicate back and forth with your main iOS app to do things like getting the user’s current location or performing an API call.

App and Watch types (7:58)

On the watch, you can make either hierarchical or page-based apps. Hierarchical is like the mail app on iOS, while page-based apps have dots on the bottom and you’re just swiping through. There’s also two different sizes of watches: 38mm and 42mm. Each has different screen resolutions, which you’ll need to know if you’re making images specifically for one display. In interface builder, you won’t need to worry about it so much because of a concept called “groups”, which are a form of replacement for auto-layout. In groups, things can’t overlap unless you set a background image for things. You can also nest within these groups.

Limitations (9:51)

Limitations of WatchKit right now include the inability to talk to any of the hardware sensors, which will include motion data or the digital crown on the side of the watch. You can’t collect UI touches, so no drawing information can be captured. Animation is also limited to more of a flipbook style. Right now the apps for the watch have all the code running on the phone, so you also have to have your phone with you if you want to look at the watch app.

For more about WatchKit limitations and tips for developers, click here.

Live Demo (11:17)

For some resources in starting to code during this tutorial, follow this link.

Hello WatchKit (11:32)

The first thing to note is that in order to use WatchKit, you have to code in XCode Beta. The stable versions do not include WatchKit. Starting with a simple single view application, the iOS simulator will run will you run the app. To get the watch app to display next to the iPhone, you have to create a new target. Activate the scheme in order to run the AppleWatch app instead of iOS apps. This also adds a couple more groups: the WatchKit extension, which runs on the phone and has the interface controller, and the WatchKit app, which is what’s stored on the watch itself and has a storyboard. Like in normal iOS apps, we can search the object library for a label and a separator. Your interfact objects can have different attributes, and everything can have a position. To hook up those objects into the code, you hold the control key and drag below the class defition in the assistant editor to create a new outlet. Then, you can set the text of that label.

class InterfaceController: WKInterfaceController {
  @IBOutlet weak var titleLabel: WKInterfaceLabel!

  override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)

        // Configure inteface objects here.

        titleLabel.setText("Hello WatchKit!")
      }

  ...
}

Map Interface (16:35)

To add a map to the app, we’ll first add a button to get to the map, and title it “view map”. We need a whole new interface controller to have a place to display the map. Inside of the interface controller we can put the map from the object library. To get from the initial interfact to the map one, I press control and drag from the button to screen, and select the push option. To get data onto the currently empty map, we’ll need a new interface controller.

To keep everything inside the WatchKit extension, highlight the old interface controller, right click, and add “new file”. Add a Cocoa Touch Class and make sure it subclasses from “WKInterfaceController”. Also ensure that it saves in Extension project, not the WatchKit App project. Now you need to create an outlet for your map - set its class through the identity inspector so that it knows which code is being referred. Then you can add the map to your assistant editor.

In the following code, CLLocationCoordinate2D sets the latitude and longitude. To display the current location, you need to get that data from the iOS app first. Setting the coordinateSpan is like setting the zoom level; we set a radius that is displayed by changes in latitude and longitude. Adding an annotation will add a pin on the map, and design guidelines suggest purple for just a single location. For directions, you should use green and red for the stard and end points of the route. You can also put an image on the map instead, so if you can have an elephant sitting on your map instead of a boring pin. Finally, the setRegion call will set the location of the map, as well as the coordinate span for the zoom level.

import Foundation
import WatchKit

class MapInterfaceController: WKInterfaceController {
  @IBOutlet weak var mapView: WKInterfaceMap!
  
  override func awakeWithContext(context: AnyObject!) {
    let location = CLLocationCoordinate2D(latitude: 37, longitude: -122)
    let coordinateSpan = MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)
    mapView.addAnnotation(location, withPinColor: .Purple)
    mapView.setRegion(MKCoordinateRegion(center: location, span: coordinateSpan))
  }
  
}

For the map to work, you do have to be connected to the internet so that it can download the tiles from Apple’s map service. Another to thing note is that you can’t drag the map around or zoom in or out; it’s just a static image. However, you can tap on it to launch the main maps app on the watch, allowing users to zoom and search for things on there.

Pages (23:26)

To make some new pages, we’ll first add a new button and a new interface controller, which will have a label and an image. Pertaining to fonts, you will ideally want to stick with either the system fonts or the text styles, which will automatically zoom based on the accessibility settings the user has set. If you do set your own custom font, you’ll have to optimize the kerning and the weight of the font so that it’ll look good in small sizes. We’ll add another interface controller to link the new one in, and so we’ll add in a new file that refers to the MapInterfaceController.

Instead of linking the new button with the new interface, we’ll launch it with code by creating an Action connection. We also need to give it an identifier inside of interface builder. We also add to the class a new property of “features” whcih will be an array of strings. We want three different interface controllers, and each will have a context set to a different feature.

class InterfaceController: WKInterfaceController {

  let features = ["Glances", "Notifications", "Apps"]

    @IBAction func browseButtonTapped() {
      //presentControllerWithName("FeatureInterfaceController", context: features[0])
      // launches a model with just a single view controller
      
      // array has a built-in function for repeating a value
      let controllers = [String](count: features.count, repeatedValue: "FeatureInterfaceController")
      // launches a model with an array of interface controllers, swipable with different pages
      presentControllerWithNames(controllers, contexts: features)      
    }
  ...
}

We need to get our images into the project. Now, both the WatchKit extension and the app have xcassets images that you can use. You’ll want to use the WatchKit app as much as possible to keep your app speedy. Images will be bundled in on your phone through the watch app and you can just refer to them, rather than transferring them over Bluetooth. Inside of our FeaturesInterfaceController, we use the if-let syntax to pass in a context if String and use that to set the Label and Image.

class FeatureInterfaceController: WKInterfaceController {
  
  @IBOutlet weak var featureLabel: WKInterfaceLabel!
  @IBOutlet weak var featureImage: WKInterfaceImage!
  
  override func awakeWithContext(context: AnyObject!) {
    if let context = context as? String {
      featureLabel.setText(context)
      featureImage.setImageNamed(context)
    }
  }
}

Questions (37:05)

Q: Can you scale images with the app?
Ben: I didn’t set the size for images on the features pages, by default it does size to fit content. Since there’s no horizontal scrolling on the watch, it automatically sizes down to where it can fit in the width of the screen. Most likely you’ll want to optimizt and size images so that they’re not wasting any space in the storage. Otherwise, if you’re generating images on the fly you would want to make sure that they’re sized only for the watch and not high resolution versions.

Q: Is there an image cache on the phone only, or both the phone and watch?
Ben: The image cache is shared between both, so there’s a copy on each and it automatically syncs in the background so you don’t have to worry about it. If it fills up, then you can pull images out so that you can add new ones in.

Q: How long will it take images to load?
Ben: Apple has actually configured the simulator so that you experience some of the same delays you would see in real life, so it’s actually kind of a good gauge for seeing how fast images will load. You’ll also notice that just loading the image that I had stored on the watch itself took a second, so if you send something from the phone to the watch it’ll probably take you even longer than that. It’s probably not going to be a very good experience.

Q: How would you include images of different sizes?
Ben: Images are set to size to fit to content, but there’s an option to set it relative to container. If I set that value to one, it’ll just scale to be as big as the display is. You can set it to say, 0.5, and set the position to center so that the image will be tight in the middle. By using relative to container, you don’t have to worry about using a fixed size on two different watches. You can also specify which images will be set for which size Watch, which can be done by checking WKInterfaceDevice for the size.

Minion Table (44:32)

After adding another button and interface controller, we add a table into the new interface. For each row in the table, we want an image and a label. After changing various settings to ensure that the image is neither stretched nor takes over the entire row, we add a table row controller that is actually a Cocoa Touch Class of NSObject.

import Foundation
import WatchKit

class MinionRowController: NSObject {
  @IBOutlet weak var minionNameLabel: WKInterfaceLabel!
  @IBOutlet weak var image: WKInterfaceImage!
  
}

To populate the whole table, we’ll need yet another controller, this time of WKInterfaceController, and create an outlet for our new class. Tables work different in WatchKit than they do in UIKit iOS apps. In WatchKit, you load the whole table at once, rather than through reusable cells as in UIKit. Apple recommends keeping the number of rows below twenty, so if you have more data than that you could just take the first twenty rows and then include a button at the bottom that says “load more rows”. Again, make sure to set the identifier so that you can refer to the storyboard in code.

import Foundation
import WatchKit

class TableInterfaceController: WKInterfaceController {
  
  let minions = ["Bob", "Dave", "Jerry", "Jorge", "Kevin", "Mark", "Phil", "Stuart", "Tim"]
  
  @IBOutlet weak var table: WKInterfaceTable!
  
  override func awakeWithContext(context: AnyObject!) {
    table.setNumberOfRows(minions.count, withRowType: "MinionRowType")
    for (index, value) in enumerate(minions) {
      if let row = table.rowControllerAtIndex(index) as? MinionRowController {
        row.image.setImageNamed(value)
        row.minionNameLabel.setText(value)
      }
    }
  }
  ...
}

The following code is called when you select a certain row in the table. You can choose what is passed into the next screen, which will be a new interface controller. In this case, it’s the minion name and image.

override func contextForSegueWithIdentifier(segueIdentifier: String, inTable table: WKInterfaceTable, rowIndex: Int) -> AnyObject? {
    return minions[rowIndex]
  }

To populate the previous view, we add yet another interface controller. We can do the same thing we did in Pages to set the images. When you override the function, you have to use AnyObject and allow any object to be passed in.

class MinionDetailsInterfaceController: WKInterfaceController {

  @IBOutlet weak var nameLabel: WKInterfaceLabel!
  @IBOutlet weak var image: WKInterfaceImage!
  
  override func awakeWithContext(context: AnyObject!) {
    if let context = context as? String {
      nameLabel.setText(context)
      image.setImageNamed(context)
    }
  }
}

There’s a lot more you can do with WatchKit, including accepting user input. You can have someone speak and it’ll translate it to text, which can be used to talk to an API. You can also use a force touch, or a hard press on the watch display that can be sensed due tot he pressure, which will pop up a menu to do things.

More Questions (1:04:55)

Q: What happens when you close the app on the iPhone?
Ben: The WatchKit Extension is still accessible even if the iOS app isn’t running. It’s in the background, but it can die.

Q: Can you have multiple Watch apps talking to the same iPhone?
Ben: Each Watch app has its own iPhone app. When you download it from the App Store, you download it as an iPhone app. You could share data as a single course using an app group, but each iPHone app only has one Watch app.

Q: To send data back and forth between extensions, would you recommend going through the app groups?
Ben: Yes. The way I do it is, from the iPhone you save data into your app group database. When the WatchKit extension is ready to display a certain screen, it can check the database and get that information.

Q: Is it okay to have the iPhone app in Objective-C, say an existing app on the phone, and the WatchKit app in Swift?
Ben: Yes, you would just have to use the same rules that you’re using to move between Swift and Objective-C in that iPhone app.

Q: When do you think the real SDK will drop?
Ben: Over time, Apple should be adding more features to the app, so maybe they’ll add more in June at WWDC. But honestly, I wouldn’t count on that and just focus on what you can build right now with this. Get creative with the tools you have.

Q: Do you have any tricks for putting apps on an actual device that you can then simulate on your wrist, as opposed to working with a simulator?
Ben: I haven’t done that yet. Before the watch came out, we had an iOS project that loaded a view that was watch-sized, and so you could run it on your iPhone and look at it on your wrist. However, you don’t get to use all the WatchKit stuff. The app Bezel will kind of simulate something of the Watch experience on the iOS simulator.

Q: How would you nest groups and add a sub-label to the table?
Ben: So for example, I want to add an age to each minion. I can get a group and drag it into the row, then put both labels inside by selecting them and dragging them into the group. If I change the group to have a vertical layout, the labels are stacked on top of each other.

Q: Is there any limit to how much hierarchical nesting that you can do?
Ben: Not that I’ve seen.

Q: Have you tried setting the WatchKit extension to subscribe to notifications?
Ben: Yes, that is possible. Natasha, who is speaking next time, is also working on an app where she’s using notifications.



Ben Morrow

Ben Morrow

Ben Morrow is a developer, author, and hackathon organizer. With the Apple Watch community, he's been working on apps made for the new device platform. Find more video and code at happy.watch­.