Passing Data Between Interfaces

To navigate directly to the Run Log page when a run is done, you need to call becomeCurrentPage of your RunLogInterfaceController. When you go back to it by calling reloadRootControllersWithNames(_:contexts:), you’re just passing the name of the controller to WKInterfaceController. You don’t get back any references to the new interface controller, so how can you call becomeCurrentPage on it?

One approach might be to use NSNotificationCenter. After you reload the controllers, you could send a notification with a short delay and then listen for that notification in your RunLogInterfaceController and call becomeCurrentPage. That would probably work, but there’s a better way. Remember the contexts parameter we were ignoring earlier? What the heck is that, anyway?

Passing Contexts in Code

Whenever you create an interface controller, you have the opportunity to pass it some context, which can be anything you want (or nothing at all). Because you don’t create interface controllers directly yourself, you can’t create different initializer methods. You could imagine, for instance, that you might solve this problem by creating a new convenience initializer on RunLogInterfaceController:

 convenience init(becomeCurrentPage: ​Bool​) {
 self​.init()
 
 if​ becomeCurrentPage {
 self​.becomeCurrentPage()
  }
 }

The merits of actually doing it that way aside, you can’t make a custom initializer, so it’s a moot point—remember, you can only create interface objects (and interface controllers) in the storyboard. Instead, you can pass a custom context object so that you can respond to it in the awakeWithContext method of your interface controller. First, let’s amend the endRun method in RunTimerInterfaceController.swift:

 func​ endRun() {
 let​ names = [​"GoRunning"​, ​"RunLog"​]
 
 let​ contexts: [​AnyObject​]?
 
 if​ ​let​ lapTimes = lapTimes, startDate = startDate {
 let​ distance = track.lapDistance * ​Double​(lapTimes.count)
 
 let​ run = ​Run​(distance: distance,
  laps: lapTimes,
  startDate: startDate)
 
  contexts = [​NSNull​(), run]
  }
 else​ {
  contexts = ​nil
  }
 
 WKInterfaceController​.reloadRootControllersWithNames(names,
  contexts: contexts)
 }

For the second item in the contexts, you’ll pass the Run object representing the run that you just finished. That NSNull is there to fill space; since the second controller in the names array receives the second context in the contexts array, you need to give something to the first one. nil can’t be in an Array, so NSNull saves the day by allowing you to put an empty placeholder there. All that aside, once you pass the run to the RunLogInterfaceController, you need to handle it on the other side. Open RunLogInterfaceController.swift and add a line to awakeWithContext that calls becomeCurrentPage:

 override​ ​func​ awakeWithContext(context: ​AnyObject​?) {
 super​.awakeWithContext(context)
 
 if​ ​let​ run = context ​as?​ ​Run​ {
  runs?.insert(run, atIndex: 0)
 
  runTable.insertRowsAtIndexes(​NSIndexSet​(index: 0),
  withRowType: ​"RunRow"​)
 
 if​ ​let​ rowc = runTable.rowControllerAtIndex(0) ​as?​ ​RunLogRowController​ {
  configureRow(rowc, forRun: run)
  }
 
  becomeCurrentPage()
  }
 }

Build and run your app; you’ll be navigated right back to the run log after completing a run. Neat! That’s not all the context parameter can do, however, and you’re in the right place to use it some more. By passing model objects around, you can use it to configure your Run Detail interface controller for an individual run.

Passing Contexts Through Storyboard Segues

Earlier,, when you configured your run log to display the Run Details interface controller, you used a storyboard segue to display the controller. You didn’t even write any code, so how are you going to send any context data? WKInterfaceController has your back with the contextForSegueWithIdentifier(_:inTable:rowIndex:) method. If you implement this method, it’ll be called as you select the run to display, and you can return a context that’ll get passed to the segue’s destination. Unlike with an iPhone app, where you’d get a reference to the UIStoryboardSegue itself and could access its destinationViewController to determine which segue this is, with WatchKit you get only the segue’s identifier. Let’s fill that in first.

Head over to the watch app’s storyboard and select the modal segue between the Run Log interface controller and the Run Details interface controller. Open Xcode’s Attributes Inspector and give the segue the identifier “ShowRunDetails.” Now that you can properly identify the segue, let’s pass some context along. Open RunLogInterfaceController.swift and implement contextForSegueWithIdentifier(_:inTable:rowIndex:):

 override​ ​func​ contextForSegueWithIdentifier(segueIdentifier: ​String​,
  inTable table: ​WKInterfaceTable​, rowIndex: ​Int​) -> ​AnyObject​? {
 if​ segueIdentifier == ​"ShowRunDetails"​ {
 guard​ ​let​ runs = runs ​where​ runs.count > rowIndex ​else​ {
 return​ ​nil
  }
 
 return​ runs[rowIndex]
  }
 
 return​ ​nil
 }

Compare the segue’s identifier with the one you set up in the storyboard; this checks whether the segue in question is the one you’re concerned with. If it is, the context you return is the run for that row, so pull it out of your runs array and send it over. For other segues, or if the index is out of bounds for your array, you’ll return nil. Next up is displaying the details of a single run.

Implementing Run Details

The RunDetailsInterfaceController class will be pretty simple, showing details for a run, down to individual lap times. In the storyboard, drag in a label and then two groups. Change the label’s text to “Run Date” so you can find it later, and then create a new @IBOutlet for it, named runDateLabel.

Change the layout of the first group to Horizontal. Add two labels, naming them “Distance” and “Pace,” and set the latter’s Horizontal position in Xcode to Right. Connect them to outlets named runDistanceLabel and runPaceLabel.

In the second group, add a label and name it “Laps:” (with the colon). This will serve as a header of sorts for the next item: a table. In the table’s row controller, add three labels, giving them the titles “Lap,” “0,” and “0:00:00,” respectively. Change the font of the middle label to Headline; this will be the number of the lap as the table is filled in. Change the 0:00:00 label on the right to be right-aligned. Connect the table to an outlet called lapsTable.

As with any row controller, you’ll need a new class for it. Rather than making a new Swift file, you can declare it in RunDetailsInterfaceController.swift, indicating that the class “belongs” to this one. As you did before with RunDetailRowController, you could create a new file for this class. Personally, I prefer keeping all of these related classes in one file, but others disagree—the choice is yours. The class itself is easy, setting only two values to configure for a row:

 class​ ​LapDetailRowController​: ​NSObject​ {
 
 @IBOutlet​ ​weak​ ​var​ lapNumberLabel: ​WKInterfaceLabel​!
 @IBOutlet​ ​weak​ ​var​ lapDurationLabel: ​WKInterfaceLabel​!
 
 lazy​ ​var​ durationFormatter: ​NSDateComponentsFormatter​ = {
 let​ durationFormatter = ​NSDateComponentsFormatter​()
  durationFormatter.unitsStyle = .​Positional
 return​ durationFormatter
  }()
 
 func​ configureForLap(lap: ​NSTimeInterval​, atIndex index: ​UInt​) {
  lapDurationLabel.setText(durationFormatter.stringFromTimeInterval(lap))
  lapNumberLabel.setText(​"​​(​index + 1​)​​"​)
  }
 
 }

Back in the storyboard, select the row controller you created. Change the class of the row controller to your new LapDetailRowController class, and then change its identifier to “LapRow.” While you’re there, be sure to connect the two @IBOutlet properties to their respective interface objects in the storyboard. Your UI is now finished. It should look like the image shown here:

images/TapALap-final-run-details-ui.png

When you want to display a run in this interface controller, you set the labels’ values appropriately and then display the laps in the table. Implement this as a new method, configureForRun:

 func​ configureForRun(run: ​Run​) {
  runDateLabel.setText(dateFormatter.stringFromDate(run.startDate))
 
 let​ lengthFormatter = ​NSLengthFormatter​()
  runDistanceLabel.setText(lengthFormatter.stringFromMeters(run.distance))
 
  lapsTable.setNumberOfRows(run.laps.count, withRowType: ​"LapRow"​)
 
 
 for​ i ​in​ 0 ..< lapsTable.numberOfRows {
 if​ ​let​ rowController = lapsTable.rowControllerAtIndex(i) ​as?
 LapDetailRowController​ {
 let​ lapTime: ​NSTimeInterval​ = run.laps[i]
  rowController.configureForLap(lapTime, atIndex: ​UInt​(i))
  }
  }
 
 let​ paceFormatter = ​NSDateComponentsFormatter​()
  runPaceLabel.setText(paceFormatter.stringFromTimeInterval(run.pace))
 }

Next, you need the dateFormatter property:

 lazy​ ​var​ dateFormatter: ​NSDateFormatter​ = {
 let​ dateFormatter = ​NSDateFormatter​()
  dateFormatter.dateStyle = .​MediumStyle
  dateFormatter.timeStyle = .​NoStyle
 return​ dateFormatter
 }()

You used a property of Run that doesn’t exist yet: pace. Open Run.swift and add it as a computed property:

 var​ pace: ​NSTimeInterval​ {
 return​ duration / ​NSTimeInterval​(laps.count)
 }

Handle the Run object that was passed in as context to awakeWithContext:

 override​ ​func​ awakeWithContext(context: ​AnyObject​?) {
 super​.awakeWithContext(context)
 
 if​ ​let​ run = context ​as?​ ​Run​ {
  configureForRun(run)
  }
 }
images/TapALap-final-run-details-ui-on-watch.jpg

And you’re all set. Build and run the app, and you’ll be able to see the Run Details screen filled in with details, just like in the image shown here:

As you can see, passing around context objects is pretty easy. This is a great example of WatchKit learning from UIKit’s experience; whereas you would need to create custom UIViewController subclasses with initializers (or public properties) to take model objects, WatchKit has a built-in method for passing data to interface controllers. Next, let’s look at passing data back from an interface controller using a protocol.

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

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