watchOS 3 Tutorial Part 2: Tables

Audrey Tam
Update Note: This tutorial has been updated to Swift 3/watchOS 3 by Audrey Tam. The original tutorial was written by Mic Pringle.

w3-feature-2Welcome back to our watchOS 3 tutorial series!

In Part 1 of this series, you learned about the basics of watchOS 3 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 Part 1 of this series. You can either continue with the same project, or download it here, if you want to start fresh.

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 is checked.

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

Add-Table

Select the Table Row Controller 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. In the Document Outline, select the group inside the table row, then use the Attributes Inspector to 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.

Next, 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, use the Attributes Inspector to make the following changes:

  • 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

The table row suddenly grows to fill the screen, but you’ll fix that now, as you layout the row!

Drag a Group from the Object Library onto the table row, 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.

Now the table row is back to a reasonable height!

Next, add a Label and an Image to this new group. You’ll configure the label, then copy and update it, to 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, then in the Attributes Inspector change Render As to Template Image.

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

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

Select the label, and set its Text to MEL. Next, change its Font to System, with a style of Semibold and a size of 20.0. Finally set its Vertical Alignment to Center.

Copy the label, then paste it on the right of the image. Change its text to SFO and its Horizontal Alignment to Right. Your table row should now look like the following:

Table-Row-Upper-Group

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, you’re 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 horizontal group. Use the Attributes Inspector to make these changes to the left 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.

With these 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 to 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. 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. Next, 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 awake(withContext:):

override func awake(withContext context: Any?) {
  super.awake(withContext: 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.png

But hey! The title is dark grey, rather than Air Aber’s vibrant pink corporate color. You’ll fix that now.

Open Watch\Interface.storyboard, select the Air Aber interface controller. In the File Inspector, change Global Tint to #FA114F.

Global-Tint

Build and run. That’s much better!

Title-Pink.png

But now, you’ll notice the rows all display the placeholder text you set in Interface Builder. You’ll fix this next, by adding a row controller 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. Make sure it’s subclassing NSObjectnot WKInterfaceController! — 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
    guard let flight = flight else { return }
    // 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(.red)
    }
  }
}

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

  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 the previous tutorial;
  2. You add a property observer that is triggered whenever the property is set;
  3. You exit early if flight is nil: it’s an optional and you want to proceed with configuring the labels only 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 color of the label to red, and update the text accordingly.

With 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.

In the Document Outline, open all the Groups in FlightRow, then right-click on FlightRow to invoke the outlets and actions popup:

Outlets-Popup

You can drag this popup to the right, so you can see all the objects in FlightRow.

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

  • destinationLabel: SFO
  • flightNumberLabel: AA123
  • originLabel: MEL
  • 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 {
  guard let controller = flightsTable.rowController(at: index) as? FlightRowController else { continue }

  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 an instance of FlightRowController. Then you set controller.flight to the corresponding flight item in the flights array. This triggers the didSet observer in FlightRowController, and configures all the labels in the table row.

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

Populated-Rows.png

Now for the final part of this tutorial: when a user taps on a table row, ScheduleInterfaceController should pass the corresponding flight as the context to the flight details interface you created in the previous tutorial, then present it.

Responding to Row Selection

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

Add the following to ScheduleInterfaceController:

override func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) {
  let flight = flights[rowIndex]
  presentController(withName: "Flight", context: flight)
}

Here, you retrieve the appropriate flight from flights using the row index passed into this method. You then present the flight details interface, passing flight as the context. Remember the name you pass to presentController(withName:context:) is the identifier you set in the storyboard, in the previous tutorial.

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

Open FlightInterfaceController.swift, and find awake(withContext:). Find this statement:

flight = Flight.allFlights().first

And replace it 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 use it to set self.flight, which will in turn trigger the property observer, and configure the interface.

For the final time in this tutorial, build and run. Tap on a table row, and you’ll now see the flight details interface 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 learned 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.

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

W2T@2xIf you enjoyed this tutorial series, you'd definitely enjoy our book watchOS by Tutorials.

The book goes into further detail on making watchOS apps and is written for intermediate iOS developers who already know the basics of iOS and Swift development but want to learn how to make Apple Watch apps for watchOS 3.

It's been fully updated for Swift 3, watchOS 3 and Xcode 8 — get it on the raywenderlich.com store today!

Audrey Tam

Audrey Tam retired at the end of 2012 from a 25-year career as a computer science academic. Her teaching included Pascal, C/C++, Java, Java web services, web app development in php and mysql, user interface design and evaluation, and iOS programming. Before moving to Australia, she worked on Fortran and PL/1 simulation software at IBM's development lab in Silicon Valley. Audrey now teaches short courses in iOS app development to non-programmers, and attends nearly all Melbourne Cocoaheads monthly meetings.

Other Items of Interest

Black Friday Sale

Starts in…

0
:
0
:
0

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!

iOS Team

... 78 total!

Android Team

... 27 total!

Unity Team

... 12 total!

Articles Team

... 15 total!

Resident Authors Team

... 20 total!

Podcast Team

... 7 total!

Recruitment Team

... 9 total!