watchOS 2 Tutorial Part 2: Tables

Mic Pringle
Note: This is a brand new tutorial released as part of the iOS 9 Feast. Enjoy!

Welcome back to our watchOS 2 tutorial series!

In the first part of this series, you learned about the basics of watchOS 2 development by creating your first interface controller.

In this second part of the series, you’ll add a table to your app that displays a list of flights.

In the process, you’ll learn:

  • How to add a new interface controller, add a table to it, and build the prototype row;
  • How to create a subclass of WKInterfaceController to populate the table, configure the rows, and handle selection;
  • How to present an interface controller modally and pass it data to present.

And with that, let’s get going! ┗(°0°)┛

Note: This tutorial picks up where we left things off in the previous tutorial. You can either continue with the same project, or download it here if you don’t have it already.

Getting Started

Open Watch\Interface.storyboard, and drag another Interface Controller from the Object Library onto the storyboard canvas, to the left of the existing Flight controller.

With the new interface controller selected, open the Attributes Inspector and make the following changes:

  • Set Identifier to Schedule;
  • Set Title to Air Aber;
  • Check Is Initial Controller:
  • Check Display Activity Indicator When Loading.

You’ll notice that the title is dark grey, rather than the vibrant pink shown in the screenshot above. You’ll fix that now:

Title-Grey

Open the File Inspector and change Global Tint to #FA114F. That’s much better:

Title-Pink

Next, drag a Table from the Object Library onto the new interface controller:

Add-Table

With the Table Row Controller selected in the Document Outline:

Table-Row-Controller

Use the Attributes Inspector to set its Identifier to FlightRow. The identifier doubles-up as the row type when you’re informing the table which rows it should be instantiating, which is why it’s important that you set it.

Building the Row’s Interface

Your first task is to make two changes to the default layout group provided by the row. Select the group inside the table row from the Document Outline, and then using the Attributes Inspector set Spacing to 6 and Height to Size To Fit Content.

Table rows have a standard, fixed height by default. However, most of the time you’ll want your rows to display all the interface objects you add to them, so it’s always worthwhile changing the Height attribute in this way.

With that out of the way, drag a Separator from the Object Library into the table row. You won’t be using it to actually separate anything, but rather to just add a little visual flair to your table row. With the separator selected, make the following changes using the Attributes Inspector:

  • Set Color to #FA114F;
  • Set Vertical alignment to Center;
  • Set Height to Relative to Container;
  • Set Adjustment to –4.

The inspector should now look like the following:

Separator

Now it’s time to flesh out the row!

Drag a Group from the Object Library onto the table row, after (to the right of) the separator. With the group still selected, change the following attributes in the Attributes Inspector:

  • Set Layout to Vertical;
  • Set Spacing to 0;
  • Set Width to Size To Fit Content.

You’ve probably noticed that you’re often manipulating the Spacing attribute; this simply tightens up the space between each of the interface objects in the group and makes things look a little sharper on the small screen.

Drag another Group into the group you just added, and make the following changes:

  • Set Spacing to 4;
  • Set Height to Fixed, with a value of 32.

To this new group, add a Label, an Image, and then another Label. These two labels will display the origin and destination of each flight.

Now you need something to put in that image. Download this image and add it to Watch\Assets.xcassets. This should create a new image set called Plane, with the actual image in the 2x slot:

Plane-Image-Set

You want to tint this image, so select the image and then in the Attributes Inspector change Render As to Template Image.

Re-open Watch\Interface.storyboard and select the image. Using the Attributes Inspector, make the following changes:

  • Set Image to Plane;
  • Set Tint to #FA114F;
  • Set Vertical alignment to Center;
  • Set Width to Fixed, with a value of 24;
  • Set Height to Fixed, with a value of 20.

Select the left label and set its Text to MAN. Also change its Font to System, with a style of Semibold and a size of 20.0. Finally set its Vertical alignment to Center.

Make the same changes to the right label, except using the text SFO. Your table row should now look like the following:

Table-Row-Upper-Group

And the interface object hierarchy should resemble the following:

Table-Row-Hierarchy-1

You’re almost done with the table row’s interface; you just need to add the flight number and status.

Drag another Group from the Object Library onto the table row, making sure it’s a sibling of the group that contains the origin and destination labels:

Table-Row-Lower-Group

As you continue to build this interface your seeing further examples of how you can use nested groups with mixed layouts to create complex layouts. Who needs Auto Layout?! ;]

Drag two labels into this new group. Once again use the Attributes Inspector to make the following changes to the left most label:

  • Set Text to AA123;
  • Set Text Color to Light Gray Color;
  • Set Font to Caption 2;
  • Set Vertical alignment to Bottom.

Now, make these changes to the right label:

  • Set Text to On time;
  • Set Text Color to #04DE71;
  • Set Font to Caption 2;
  • Set Horizontal alignment to Right;
  • Set Vertical alignment to Bottom.

And with those final changes, the completed table row should now look like this:

Table-Row-Complete

Now the table is set up in Interface Builder it’s time to populate it with some data.

Populating the Table

The first thing you need to do is create a subclass of WKInterfaceController that will provide the data for the table.

Right-click on the Watch Extension group in the Project Navigator and choose New File…. In the dialog that appears select watchOS\Source\WatchKit Class and click Next. Name the new class ScheduleInterfaceController, and make sure it’s subclassing WKInterfaceController and that Language is set to Swift:

File-Options

Click Next, and then Create.

When the new file opens in the code editor, delete the three empty method stubs so you’re left with just the import statements and the class definition.

Re-open Watch\Interface.storyboard and select the new interface controller. In the Identity Inspector, change Custom Class\Class to ScheduleInterfaceController:

Custom-Class

With the interface controller still selected, open the Assistant Editor and make sure it’s displaying ScheduleInterfaceController. Then, Control+Drag from Table in the Document Outline to inside the class declaration of ScheduleInterfaceController to create an outlet:

Table-Outlet

Name the outlet flightsTable, make sure the type is set to WKInterfaceTable and click Connect.

Now you’ve set the custom class and created an outlet to the table, it’s finally time to populate the thing!

Close the Assistant Editor, open ScheduleInterfaceController.swift, and add the following, just below the outlet:

var flights = Flight.allFlights()

Here you’re simply adding a property that holds all the flight information as an array of Flight instances.

Next, add the following implementation of awakeWithContext(_:):

override func awakeWithContext(context: AnyObject?) {
  super.awakeWithContext(context)
  flightsTable.setNumberOfRows(flights.count, withRowType: "FlightRow")
}

Here you’re informing the table to create an instance of the row you just built in Interface Builder for each flight in flights. The number of rows is equal to the size of the array, and the row type is the identifier you set in the storyboard.

Build and run. You’ll see a table populated with several rows:

Identical-Rows

However, you’ve likely noticed that they’re all displaying the placeholder text you set in Interface Builder. You’ll fix this now by adding a row controller that you can use to configure the labels for each row individually.

Adding a Row Controller

Right-click on the Watch Extension group in the Project Navigator and choose New File…. In the dialog that appears select watchOS\Source\WatchKit Class and click Next. Name the new class FlightRowController, and make sure it’s subclassing NSObject and that Language is set to Swift:

File-Options-Row-Controller

Click Next, and then Create.

When the new file opens in the code editor add the following to the top of the class:

@IBOutlet var separator: WKInterfaceSeparator!
@IBOutlet var originLabel: WKInterfaceLabel!
@IBOutlet var destinationLabel: WKInterfaceLabel!
@IBOutlet var flightNumberLabel: WKInterfaceLabel!
@IBOutlet var statusLabel: WKInterfaceLabel!
@IBOutlet var planeImage: WKInterfaceImage!

Here you’re simply adding an outlet for each of the labels you added to the table row. You’ll connect them shortly.

Next, add the following property and property observer just below the outlets:

// 1
var flight: Flight? {
  // 2
  didSet {
    // 3
    if let flight = flight {
      // 4
      originLabel.setText(flight.origin)
      destinationLabel.setText(flight.destination)
      flightNumberLabel.setText(flight.number)
      // 5
      if flight.onSchedule {
        statusLabel.setText("On Time")
      } else {
        statusLabel.setText("Delayed")
        statusLabel.setTextColor(UIColor.redColor())
      }
    }
  }
}

Here’s the play-by-play of what’s happening here:

  1. You declare an optional property of type Flight. Remember, this class is declared in Flight.swift, which is part of the shared code you added to the Watch Extension in tutorial #1;
  2. You add a property observer that is triggered whenever the property is set;
  3. You exit early if flight is nil. Since it’s an optional and you only want to proceed with configuring the labels when you know you have a valid instance of Flight;
  4. You configure the labels using the relevant properties of flight;
  5. If the flight is delayed then you change the text colour of the label to red and update the text accordingly.

With the the row controller set up, you now need to update the table row to use it.

Open Watch\Interface.storyboard and select FlightRow in the Document Outline. Using the Identity Inspector, set Custom Class\Class to FlightRowController.

Next, right-click on FlightRow in the Document Outline to invoke the outlets and actions popup:

Outlets-Popup

First connect planeImage to the image in the table row, and separator to the separator. Then connect the remaining outlets as per the list below:

  • destinationLabel: SFO
  • flightNumberLabel: AA123
  • originLabel: MAN
  • statusLabel: On time

The final step is to update ScheduleInterfaceController so it passes an instance of Flight to each row controller in the table.

Open ScheduleInterfaceController.swift and add the following to the bottom of awakeWithContext(_:):

for index in 0..<flightsTable.numberOfRows {
  if let controller = flightsTable.rowControllerAtIndex(index) as? FlightRowController {
    controller.flight = flights[index]
  }
}

Here you’re iterating over each row in the table using a for loop, and asking the table for the row controller at the given index. If you successfully cast the controller you’re handed back to an instance of FlightRowController, you set flight to the corresponding flight in flights. This in-turn triggers the didSet observer and configures all the labels in the table row.

It’s time to see the fruits of your labour. Build and run. You’ll see that your table rows are now populated with the relevant flight details:

Populated-Rows

The final part of this tutorial is to present the flight details interface you created in tutorial #1 when a user taps on a table row, and pass it the corresponding flight as the context.

Responding to Row Selection

The first thing you need to do is to override the method declared WKInterfaceController that’s responsible for handling table row selection.

Add the following to ScheduleInterfaceController:

override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
  let flight = flights[rowIndex]
  presentControllerWithName("Flight", context: flight)
}

Here you retrieve the appropriate flight from flights using the row index that’s passed to this method. You then present the flight details interface, passing flight as the context. Remember that the name you pass to presentControllerWithName(_:context:) is the identifier you set in the storyboard.

Now you need to update FlightInterfaceController so it uses the context to configure its interface.

Open FlightInterfaceController.swift and find awakeWithContext(_:). Replace this statement:

flight = Flight.allFlights().first!

With the following:

if let flight = context as? Flight { self.flight = flight }

Here you try to cast context to an instance of Flight. If it succeeds you then use it to set self.flight, which will in turn trigger the property observer and configure the interface.

For the final time in the tutorial, build and run. If you tap on a table row, you’ll now see the flight details interface is presented modally, displaying the details of the selected flight:

Final

Congratulations! You’ve now finished implementing your very first table, and have populated it using real data; nice work!

Where to Go From Here?

Here is the finished example project from this tutorial series so far.

In this tutorial you’ve learnt how to add a table to an interface controller, build the table row interface, create a row controller, handle table row selection, present another interface controller, and even pass contexts. Phew! That’s a lot to cram into 20 minutes or so.

So, where to next? Part 3 of this tutorial, of course! There, you’ll learn all about animation in watchOS.

You might also be interested in our book watchOS 2 by Tutorials that goes into much greater detail about making watchOS 2 apps – from beginning to advanced.

If you have any questions or comments on this tutorial, please join the forum discussion below! :]

Mic Pringle

Mic Pringle is a developer first and foremost, but regularly turns his hand to book and tutorial editing, video tutorial making, and hosting the official raywenderlich.com podcast. Mic is 1/6th of Razeware, the team behind raywenderlich.com. An iOS developer by trade, Mic has recently found himself dabbling in Ruby, PHP, and JavaScript.

When not knee-deep in code, Mic enjoys spending time with his wife Lucy and their daughter Evie, watching his beloved Fulham F.C., and weather permitting, tearing up the English countryside on his Harley Davidson.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

Swift Team

... 15 total!

iOS Team

... 44 total!

Android Team

... 15 total!

macOS Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 15 total!

Resident Authors Team

... 17 total!

Podcast Team

... 8 total!

Recruitment Team

... 9 total!