Creating Your Apple Watch App

While going for a run, you want to know things like how far you’ve traveled and how fast you’re moving. Plenty of apps exist to do this outside, where your iPhone gets great GPS reception, but you want one that works great indoors on a track. That is the purpose of the Apple Watch app we’ll be creating together in this book. You’ll be building a rudimentary stopwatch; every time users cross the starting line of the lap, they’ll tap a button to mark a lap as completed, and the app will mark the time they tapped the button. Inspired by this user interface, the app is named TapALap, since you’re tapping a lap. I know—amazing name. Every track is a different size; the one at my local gym, for instance, is five laps to a mile. So your app will need a screen to start and stop a run as well as mark a lap as complete, and another screen to specify how long your track is.

Creating the Xcode Project

To get started, open Xcode and select File New Project. Select the iOS App with WatchKit App template under the watchOS section. On the next screen, enter TapALap as the product name. I’ll use Pragmatic Bookshelf and com.pragprog as the organization name and identifier, but use whatever you’d like. We’ll use Swift for the language. Underneath the language selection, uncheck all of the optional check boxes. Finally, make sure iPhone is the selected device. Choose Next, find a place to save the app, and choose Create.

Joe asks:
Joe asks:
Why Aren’t We Including Tests?

You may have noticed two check boxes that we disabled when we created the TapALap project: Include Unit Tests and Include UI Tests. Those sound like good things to have, so why not enable them?

Fact is, WatchKit is still a very young platform, and the tools in Xcode don’t really support tests yet. You can’t run UI tests on your WatchKit app—they’re limited to iOS and OS X apps for now—and your unit tests can’t target your WatchKit extension. If you want to have tested code in your WatchKit app, you’ll need to extract it into a framework for which you can run unit tests. It’s definitely not an ideal situation, so hopefully future versions of Xcode will be able to do more watchOS testing.

The first thing you need in this app is a screen to start the run. This will be the app’s main task, so you want that screen to be the first thing users see when they open it. By default, Xcode created an interface controller class called, appropriately enough, InterfaceController.

Renaming the Interface Controller

You’ll have numerous interface controllers by the time this is done, so before we continue, let’s rename this class. To give InterfaceController a better name, select InterfaceController.swift in Xcode’s Project Navigator (it should be in the TapALap WatchKit Extension group) and then press to rename it as GoRunningInterfaceController.swift.

Once the file is renamed, you need to rename the class. Open GoRunningInterfaceController.swift and rename the class so its declaration looks like this:

 class​ ​GoRunningInterfaceController​: ​WKInterfaceController​ {
 
 }

Next, you need to rename it in the storyboard. Open Interface.storyboard and select the interface controller. In the Identity Inspector (3), change the Class value to GoRunningInterfaceController. Now the three-step renaming process is complete! This tedious process is a sign of Swift’s relative immaturity compared to other languages; once the Xcode tools around it mature, renaming classes and other refactoring tasks should get a lot easier.

Creating Your Interface

It’s time to start adding UI elements. The most important is a button to start running—after all, isn’t that the point of the app? Open the Interface.storyboard file again and select the interface controller. To better identify it later when you have others, select and title it “Go Running” in the Attributes Inspector. Next, open Xcode’s Object Library (3) and drag a new WKInterfaceButton (named Button in the library’s list) onto the interface controller. This will be your Start Run button, title it “Start Run” in the Attributes Inspector and set its background color to green. This button is set for now—you’ll hook up this button to some code later. This interface controller is also complete for now—in the next chapter, you’ll add a more complex UI to it.

Aside from a screen to start running, you need a screen to display when the user is on a run. In your storyboard, open Xcode’s Object Library again and drag out a new interface controller. Use the Attributes Inspector to set its title to “Run.” Add a new button to it with a red background; this will be the Stop Run button, so set that as its title. You also want a button to mark a lap as finished, so drag in a new button and give it the title “Finish Lap.” Finally, your users will want to know how far they’ve run, so add two more labels to the controller. Set the first one’s text style to Headline and its title to “Total Distance:” (with the colon). Set the second label’s text style to “Subhead.” Since you’ll set its value in code, leave its text alone for now.

Create a new WKInterfaceController subclass for this interface controller, named RunTimerInterfaceController. Select the interface controller in the storyboard and change its class in the Identity Inspector (3) to RunTimerInterfaceController. It is time to hook up some code to the buttons. Open Xcode’s Assistant Editor () and select the interface controller in your storyboard. If the interface-controller class doesn’t appear in the Assistant Editor, reset it with Z.

Connecting UI and Code

When you’re finished with this project, the Stop Run button will finish the run and transition to a different screen, but for now let’s just hook it up to the code. Control-drag from the button to your interface controller’s code—between the curly braces—and select Action from the Connection drop-down, naming it stopRunButtonPressed. You’ll have that method call endRun, another method you’ll create in your interface controller and in which you’ll be implementing a lot of this app’s logic:

 @IBAction​ ​func​ stopRunButtonPressed() {
  endRun()
 }
 
 func​ endRun() {
 
 }

Next, you want a way to update your Total Distance label. To do that, you need a way to reference the label from code. Control-drag from the second label to the top of the class. Choose Outlet from the Connection drop-down and name it distanceLabel. You don’t need an outlet for the first label; its text doesn’t change. Now you can update the label’s text using its setText method. But how do you know how far one lap is? You need some data in your object—a Track struct would be prudent. You don’t need to do much more than store data in it, so using a struct instead of a class helps cut down on the code you need to write.

Creating Data Structures

In Xcode, select File New File, and choose the Swift File template. Name the file Track.swift. Ensure that the file is added to the TapALap WatchKit Extension target, and then create it. Once you’ve created the file, implementing the struct is short—it only needs to store a name and a lap distance:

 struct​ ​Track​ {
 let​ name: ​String
 let​ lapDistance: ​Double​ ​// in meters
 }

Later you’ll allow users to configure their own tracks. For now you’ll create a temporary Track instance for use in your calculations. Head back to RunTimerInterfaceController.swift and add a new property to store the Track. You’ll make it lazy so that it’ll be created as needed:

 lazy​ ​var​ track: ​Track​ = ​Track​(name: ​"Track"​, lapDistance: 500)

This creates a track named Track, with a distance per lap of 500 meters. When the user taps the Finish Lap button, you’ll add 500 meters to the total distance, marking the time at which the lap is finished. All you really need to store is how long each lap was in terms of time, since you know the distance is the same for each lap. You can simply add an array of NSTimeInterval values for each lap:

 var​ lapTimes: [​NSTimeInterval​]?
 var​ startDate: ​NSDate​?
Joe asks:
Joe asks:
When Is willActivate Called?

Interface controllers, much like their view controller counterparts, have a set of lifecycle methods that your subclasses can implement to customize their functionality. There are far fewer for WKInterfaceController, but the important ones are there. Here they are, in order:

  1. init—Called when an interface controller instance is created. Use it to perform any one-time setup you need to do.

  2. awakeWithContext—Called when an interface controller is initialized, optionally with contextual information provided.

  3. willActivate—Called immediately before the interface controller appears on the screen. Use this method to ensure that the data presented by the interface controller is up to date.

  4. didDeactivate—Called when the interface controller disappears. Use this method to clean up any resources used by the interface controller.

You don’t need to implement each of these lifecycle methods in each interface-controller class you make, but it’s a good idea to be familiar with them and to know which tasks belong in which method.

You’ll want to initialize this array at the beginning of the user’s run, which is right when this interface controller appears onscreen. To do that, implement the interface controller’s willActivate method. You’ll want to do this only if the array is currently nil; if the user leaves your app and comes back to it, willActivate will be called again, and you’ll want to preserve the user’s current run. Add the method now:

 override​ ​func​ willActivate() {
 super​.willActivate()
 
 if​ lapTimes == ​nil​ || startDate == ​nil​ {
  lapTimes = []
  startDate = ​NSDate​()
  }
 }

Every time a user finishes a lap, you want to add the lap’s duration to the lapTimes array and update the total distance label. To prepare for this, add a new method called updateDistanceLabel. You’ll use the lapTimes array again here, multiplying its count by the track’s per-lap distance. You’ll create a lazy NSLengthFormatter to turn the distance into a human-readable string:

 lazy​ ​var​ lengthFormatter: ​NSLengthFormatter​ = {
 let​ formatter = ​NSLengthFormatter​()
  formatter.numberFormatter.maximumSignificantDigits = 3
 return​ formatter
 }()
 
 func​ updateDistanceLabel() {
 guard​ ​let​ lapTimes = lapTimes ​where​ !lapTimes.isEmpty ​else​ {
  distanceLabel.setText(​"No laps finished!"​)
 return
  }
 
 let​ distance = track.lapDistance * ​Double​(lapTimes.count)
 
  distanceLabel.setText(lengthFormatter.stringFromMeters(distance))
 }

Now all you need to do is to connect a new @IBAction method for the Finish Lap button in your storyboard:

 @IBAction​ ​func​ finishLapButtonPressed() {
 let​ lapFinishTime = ​NSDate​()
 
 guard​ ​let​ startDate = startDate, lapTimes = lapTimes ​else​ { ​return​ }
 
 let​ totalRunDuration = lapFinishTime.timeIntervalSinceDate(startDate)
 
 let​ cumulativeLapDuration = lapTimes.reduce(0, combine: { $0 + $1 })
 
 let​ lapDuration = totalRunDuration - cumulativeLapDuration
 
 self​.lapTimes?.append(lapDuration)
 
  updateDistanceLabel()
 }

When the user taps Finish Lap, the label will update. There’s only one more thing left to do. Add a call to updateDistanceLabel in the willActivate method:

 override​ ​func​ willActivate() {
 super​.willActivate()
 
 if​ lapTimes == ​nil​ || startDate == ​nil​ {
  lapTimes = []
  startDate = ​NSDate​()
  }
 
  updateDistanceLabel()
 }
images/TapALap-RunTimerInterfaceControllerWithDistanceLabel.jpg

Now when you come to this screen, the distance label will be set at 0. Once the Finish Lap button is tapped, it will update automatically.

That’s a good place to stop for now. To try out this interface controller, drag the Initial Controller arrow in the storyboard to it, and then run your app. As you tap Finish Lap, you will see the label update—your screen should look like the one shown here. Be sure to drag the arrow back to the Go Running interface controller when you’ve finished.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.104.17