Concentric Sky

Menu

Kurt Mueller // January 22, 2015

Creating Apps for Apple Watch

apod_watch.png image

After years of rumors and hype, Apple announced the Apple Watch to much fanfare in September, 2014. Though it will not be available for purchase until sometime later in the first quarter of 2015, Apple developers can start working on apps for the Watch now, with Xcode 6.2 Beta available from developer.apple.com. I wanted to learn more about developing for Watch and about Swift, Apple’s new programming language, so I wrote a Watch app in Swift to accompany APOD, a popular iOS app we created here at Concentric Sky. (In related news, we launched APOD for Android this week.) Here’s a brief explanation of how I created the Watch app for the APOD iOS app.

Apple Watch Development Basics

First, let’s talk about current development options and restrictions for Apple Watch. As of today, all third-party Watch apps must have a corresponding iPhone app that handles most of the heavy programmatic lifting. You can’t write a Watch app that runs entirely on the Watch hardware, without a communicating iPhone app, though it’s likely that this restriction will be eased over time as the platform matures and developer tools are improved. For now, only Apple can make Watch apps that run without an iPhone and corresponding phone app. 

As explained in more detail by Apple here, Watch apps support three types of interfaces: full-app interactions, glances, and notifications. A full-app interface is required, while glances and notifications are optional. In this article I will discuss creating a full (but simple) interface, and I will address glances and notifications in subsequent articles.

A Watch App for APOD

APOD displays astronomy pictures from the APOD repository, along with titles and descriptions. The iOS APOD app has a gallery view (implemented as a UICollectionView) and a single-image view. Given the small size of the Watch screen, it made the most sense to create a single-image view first, before trying to show multiple images. However, I decided to display the single image as a table with one cell, to facilitate showing multiple images at some later point. I wanted an image and a title label to fill up the entire watch display:

Create Watch Targets

Watch apps are implemented as App Extensions to iOS apps, using the new WatchKit framework available in Xcode 6.2 beta. The first step in creating a Watch app is to make WatchKit Extension and WatchKit App targets in your iOS app, using File / New / Target and selecting the Apple Watch template:

In the options window, I choose Swift as the language for my new target, and I check the boxes for “Include Notification Scene” and “Include Glance Scene.” Checking these boxes causes Xcode to create stub Controller classes for notifications and glances, and add scenes for these to the Watch storyboard it creates.

Now in Project Navigator I see the new targets:

The WatchKit Extension is for the code that runs on the iPhone to support the Watch app, and the WatchKit App target has the storyboard and image assets file for the Watch. You can see that there are no .swift files in the WatchKit App target, which makes sense given that it is not possible for third-party developers to create code that runs directly on Watch at this point. We can define user interfaces for Watch, but the code that controls those interfaces runs on the phone. The full app is controlled by InterfaceController.swift. The NotificationController and GlanceController will be tackled later.

Configuring the Storyboard

Looking at the WatchKit App’s Interface.storyboard, there are four scenes, but I am only concerned with the Interface Controller Scene:

I would like to display a single image with a two-line label under it for the image’s title, and to make the future goal of displaying multiple images easier, I will make a table with a single row. The WatchKit table class is called WKInterfaceTable. There’s a Table object in the Objects library in Interface Builder:

Dragging a Table to the Interface Controller Scene results in:

Within the new Table, there’s a Table Row Controller. This is conceptually similar to a prototype cell in a UITableView or UICollectionView. The Table Row Controller is backed by a custom row controller class that has outlets for each of the UI objects within the table row that you wish to update when displaying the table. In this case, I want an image and a label, with the image above the label. You can see that the table row has a Group item, which is a WKInterfaceGroup. This group will contain the image and the label and determine how they are displayed. To keep things simple, Watch layouts don’t use constraints like iOS storyboards. Instead, a group can either have a horizontal or vertical layout, much like Android’s LinearLayout, and it will display contained items from left to right (for horizontal layouts) or top to bottom (for vertical layouts). I want a vertical layout with the image appearing above the title, so I adjust the group’s Attributes:

I’ve given the group a vertical layout, set Custom Insets to 0 so that the image and label will be flush up against the edges of the display, and set the Size Height to be Relative to Container, with a value of 1. This makes the group take up the entire vertical space of the container, which is its table row.

Next I add an Image object from the Objects library, inside the group:

For the image Size, I set the Width and Height to Relative to Container, with the Width filling the container (value of 1) and the Height taking up 75% of the container height (value of 0.75). This leaves enough room under the image for a two-line label:

The last step in designing the UI is to set the image and label to sensible defaults to indicate that an image is loading, for display before I set the actual APOD image and title in code. I do this by adjusting the Image attribute of the image and the Text attribute of the label (I first add a default image to my WatchKit App’s Images.xcassets file):

Adding a Custom Row Controller

Next I create a Table Row Controller class to provide IBOutlets so I can set the row’s image and label text at runtime. I create a new Swift file called APODRowController.swift:

This file extends NSObject, and has IBOutlets for the image and label defined in the storyboard, above. It also has an apodKey variable to keep track of which APOD is displayed by the row, and has a configureCell() method to pass in the key, title text, and image and set the image and title text in the displayed row.

Now that I have a custom class to back the table row, I must tell the storyboard about the custom class and make the IBOutlet connections from the class to the image and label. These are the Identity, Attributes, and Connections inspectors for the Table Row Controller after I update it:

In the Identity inspector, I set the Custom Class to my newly-created custom class, APODRowController. In the Attributes inspector, I change the name of the row controller identifier to “default,” which will be used later when I configure the row in the interface controller. And in the Connections inspector, you can see the Outlet connections I made from the row controller IBOutlets to the image and label in the storyboard.

Bringing it all Together with InterfaceController

Finally I am ready to flesh out the boilerplate InterfaceController.swift class. If this were a regular iOS UITableView controller, I would implement various methods in the UITableViewControllerDatasource and UITableViewControllerDelegate protocols to configure the number of sections and rows in the table, create each row of the table, etc. However, WatchKit tables are much simpler, and because I am only displaying a single row in my table, simpler yet. The number of rows for a Watch table must be set and each row needs to be configured up front when the table is loaded. If my table had multiple rows I would iterate through them, configuring each one, but since I have just one row I don’t have to loop at all. Here’s the entire class:

The class contains an IBOutlet for the WKInterfaceTable I created in the Watch app storyboard, which I connected in the storyboard from the Interface Controller to the table. It also has a reference to an ApodService class, defined elsewhere and beyond the scope of this blog post, that performs asynchronous loading of today’s APOD. The APODService has a single public function that takes a completion handler:

I am only displaying a single row, backed by an APODRowController object, and I keep a reference to that row called todayCell to enable configuration of it after the asynchronous loading of today’s APOD is complete.

I override the WKInterfaceController superclass function awakeWithContext() to call loadTable(). loadTable() first tells the table that it will have a single row, and that row is of type “default” (recall that I defined my row controller Identifier attribute to be “default” in the storyboard). Then I ask the tableView to give me an APODRowController object and assign it to my todayCell variable. Next, I call the APODService function to load today’s APOD and pass in a completion handler block that configures the row with the resulting key, title, and image. And finally, another call to the tableView’s setNumberOfRows function causes the tableView to redraw, displaying the updated row.

The first time you run a Watch app in the simulator, you must wait for the simulator to launch and then go to Hardware / External Displays and select one of the two Apple Watch displays (38mm or 42mm). Then you will see the Watch simulator appear. This is what I see for my simple APOD display:

Next Steps

This is a very simple example, and only scratches the surface of Watch interfaces and interactivity. Maybe I want to view previous APOD images, or get notifications on my watch when a new APOD image is available. Perhaps I want to share my favorite APOD images with friends through social media or messaging. Wouldn’t it be nice if the APOD watch app knew what I last viewed in the iOS app and could automatically show it to me on the watch? Maybe I want to look through images on the watch and then fling one to my phone to check it out on the bigger display. The possibilities for novel and useful interactions between watch and phone are endless.

We are very excited here at Concentric Sky about wearables and we can’t wait to get our hands on actual Apple Watch hardware in the next couple of months. In the meantime, we are busy exploring the developer tools and adding support to our apps in anticipation of the big launch. Check back for more Apple Watch news, as this is sure to be a hot topic.

Code from this post:

//
// APODRowController.swift
// APOD
//
// Created by Kurt Mueller on 1/18/15.
// Copyright (c) 2015 Concentric Sky, Inc. All rights reserved.
//
import Foundation
import WatchKit
class APODRowController : NSObject {
  @IBOutlet weak var apodImage : WKInterfaceImage!
  @IBOutlet weak var titleLabel : WKInterfaceLabel!
  var apodKey: String?

  func configureCell(key : String, title: String, image: UIImage) {
    apodKey = key
    titleLabel!.setText(title)
    apodImage!.setImage(image)
  } 
}

//
// InterfaceController.swift
// APOD WatchKit Extension
//
// Created by Kurt Mueller on 1/18/15.
// Copyright (c) 2015 Concentric Sky, Inc. All rights reserved.
//
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {

  @IBOutlet weak var tableView : WKInterfaceTable!

  var apodService = APODService()
  var todayCell : APODRowController! = nil
  override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)

    loadTable()
  }

  private func loadTable() -> Void {
    tableView.setNumberOfRows(1, withRowType: "default")
    todayCell = tableView.rowControllerAtIndex(0) as APODRowController

    apodService.currentApodInfo { (failed, title, image) in
      self.todayCell.configureCell(self.apodService.currentApodKey(), title: title!, image: image!)
      self.tableView.setNumberOfRows(1, withRowType: "default")
    }
  }
}
comments powered by Disqus