The Interface Controllers

With the PBLocationManager in place, we have done the heavy lifting of the app. The interface controllers are very slim in design and simple in implementation.

Create the InterfaceController class

Select the InterfaceController.swift file that was created as part of the Xcode template and replace all of the code in it (including the import statement) with the following:

Import WatchKit

class InterfaceController: WKInterfaceController, PBLocationManagerDelegate {

    var locationManager: PBLocationManager! //1

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        locationManager = PBLocationManager(delegate: self) //2
        locationManager.requestLocation() //3
    }

    func handleNewLocation(newLocation: CLLocation) { //3
        print(newLocation)
    }

    func handleLocationFailure(error: NSError) { //4
        print(error)
    }

}

What we are doing here is writing the bare minimum code necessary to test the PBLocationManager class. Some of this code we will remove once we are satisfied that the Core Location data is successfully being obtained.

The comments in the code are as follows:

  1. We declare a property, locationManager, that is an instance of our PBLocationManager class. By using the exclamation mark, we are creating an implicitly unwrapped optional, which basically means that instead of creating an optional value and testing that it is not an empty optional when we need to use it, we promise the compiler that it will exist by the time we call it.
  2. How can we promise this? We initialize an instance in the awakeWithContext method, by which we can be sure that locationManager never equals nil. If it did, the app would crash.
  3. We fire off a call to the locationManager to request the current location.
  4. If the locationManager receives its data, it will call this method on its delegate. For the moment we just print the results to the console for debugging

Have a good look through this little snippet of code; it's something you'll be doing quite often as a developer. It is much better to ensure that the code is working as you expect it to now, in isolation, rather than later when there is a ton of other code that could also be responsible for things not working out as they should.

Test in the console

Okay, we're nearly ready to run a test, but first we need to select the right Scheme to the right of the Run button in Xcode; we need the Plot Buddy WatchKit Extension Scheme.

Now hit Run.

Note

If nothing happens, check that you have entered the NSLocationWhenInUseUsageDescription XML entry into the Info.plist of the iPhone app, not the WatchKit app.

Your Watch Simulator or your device should now ask you for permission to access your location data, as pictured here:

Test in the console

Whether or not you press Dismiss doesn't really matter, what counts is that you allow access on your iPhone or Simulator, as illustrated below:

Test in the console

It will come as no surprise to you that you should select Allow.

When you do so, the dialog box will also disappear from the watch screen. But nothing more will happen yet—the call to requestLocation came and went long before you tapped on Allow.

So let's run it again. This time, the request for data is made. After a few seconds, the response that you've logged to the console (nothing will appear on the watch screen yet) may or may not look something like this:

<+37.33233141,-122.03121860> +/- 5.00m (speed 0.00 mps / course -1.00) @ 11/23/15, 8:16:54 PM New Zealand Daylight Time

If it does, congratulations! You have successfully harvested a bunch of location data from watchOS. If the response you get is this:

Error Domain=kCLErrorDomain Code=0 "(null)"

then you are probably using the Watch Simulator and you'll need to let it know where you would like the Simulator to pretend to be: Go to Watch Simulator, select Debug | Location and choose Apple from the list of location data that the simulator can fake for you. Run the app again.

Beware of the glitches

At the time of writing, Xcode is still displaying some growing pains when it comes to the Watch Simulator. It may take several attempts to get it to accept the new location you select, so you may have to alternate between Apple and None a couple of times before the dreaded null response stops plaguing you.

When testing the location code you may also come across cached responses, which will show an incorrect time stamp, though the location is correct. Don't let this bother you, on the device it all works fine.

Code

Okay, now that we have PBLocationManager doing what it should, we can dispense with the test code and start to implement InterfaceController for real.

We start off with some string constants that will specify button titles and label text. Add the following code after the import statement, but before the InterfaceController class definition:

Let kStartPlotting = "Start
Plotting"
Let kStopPlotting = "Stop
Plotting"
Let kStoreCoordinates = "Add Current
Coordinates"
Let kFetching = "Fetching Location..."
Let kLocationFailed = "Location Failed"

The implementation of the class contains very little that you have not seen before. Replace the entire InterfaceController class code with the following:

Class InterfaceController: WKInterfaceController, PBLocationManagerDelegate {
    
    var locationManager: PBLocationManager!
    var isRecording = false

    @IBOutlet var startStopGroup: WKInterfaceGroup!
    @IBOutlet var addPlotGroup: WKInterfaceGroup!
    @IBOutlet var showPlotsGroup: WKInterfaceGroup!
    @IBOutlet var showPlotsButton: WKInterfaceButton!
    @IBOutlet var infoLable: WKInterfaceLabel!
    @IBOutlet var startStopButton: WKInterfaceButton!
    @IBOutlet var addPlotButton: WKInterfaceButton!

    Override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        locationManager = PBLocationManager(delegate: self)
        updateUI(running: false)
    }

    @IBAction func startStopButtonPressed() {
        toggleRecording()
    }
    @IBAction func addPlotButtonPressed() {
        fetchLocation()
    }

    @IBAction func showPlotsButtonTapped() {
        pushControllerWithName("PlotsScene", context: locationManager.currentLocations)
    }

    @IBAction func sendPlotsButtonTapped() {
WatchConnectivityManager.sharedManager.sendDataToWatch(locationManager.currentLocations)
    }

    func toggleRecording() {
        isRecording = !isRecording
        if isRecording {
            locationManager.clearLocations()
        }
        updateUI(running: isRecording)
    }

    func fetchLocation() {
        updateUI(fetching: true)
        locationManager.requestLocation()
    }

    func handleNewLocation(newLocation: CLLocation) {
        addPlotButton.setTitle(kStoreCoordinates)
        infoLable.setText("Lat:
" + "(newLocation.coordinate.latitude)" + "
Long:
" + "(newLocation.coordinate.longitude)")
updateUI(fetching: false)
    }

    func handleLocationFailure(error: NSError) {
        infoLable.setText(kLocationFailed)
        updateUI(fetching: false)
    }

    func updateUI(running running: Bool) {

        infoLable.setText("")
        infoLable.setHidden(!running)
        startStopButton.setTitle(isRecording ? kStopPlotting : kStartPlotting)

        showPlotsGroup.setHidden(running)
        showPlotsButton.setEnabled(locationManager.currentLocations.count>0)
showPlotsGroup.setBackgroundColor(locationManager.currentLocations.count>0 ? UIColor.blueColor() : UIColor.clearColor())

        addPlotGroup.setHidden(!isRecording)
    }

    func updateUI(fetching fetching: Bool) {
        infoLable.setHidden(fetching)

        startStopButton.setEnabled(!fetching)
        startStopGroup.setBackgroundColor(fetching ? UIColor.clearColor() : UIColor.blueColor())

        addPlotButton.setEnabled(!fetching)
        addPlotGroup.setBackgroundColor(fetching ? UIColor.clearColor() : UIColor.blueColor())
        addPlotButton.setTitle(fetching ? kFetching : kStoreCoordinates)
    }
}

Don't worry about the compiler error, this is happening because we have not yet added the WatchConnectivityManager class.

This class implements the methods we specified in our PBLocationManagerDelegate protocol definition.

func handleNewLocation(newLocation: CLLocation)
func handleLocationFailure(error: NSError)

This is the promise we make when we declare this class to conform to the protocol.

It's worth pointing out the method signatures of the last two methods. Why is the argument name written twice?

func updateUI(fetching fetching: Bool)

This forces the code that calls the method to explicitly name the argument being passed in this form:

updateUI(fetching: false)

This is much clearer, and makes understanding the code (when you return to it a year from now) very much easier, and is more elegant than including the name of the argument in the actual name of the method updateUIForFetching(false).

Apart from this, the InterfaceController class is familiar territory, and does nothing more than pass method calls and data to and from the PBLocationManager class, and is thus a good illustration of the separation of concerns. The controller knows nothing about where the data comes from or how it is formatted; it only needs to be supplied with the right string data. However, you should read through it carefully, making sure that you understand what is going on.

Interface

Now we must return to Interface Builder to create the user interface.

  1. In the project navigator, select the Interface.storyboard file, and ensure that the Assistant Editor is open (Command-Option-Return).
  2. Drag a Menu object onto the interface and select the single Item in the Organizer, as illustrated here:
    Interface
  3. In the Attributes Inspector, change the Title to Send Plots and the Image to Share.
  4. Connect the Send Plots menu item to @IBAction func sendPlotsButtonTapped()in the source code in the Assistant Editor.

Now we must add the Group objects to the interface, given as follows.

  1. Drag a Group onto the interface and connect to the @IBOutlet var startStopGroup in the source code.
  2. Set the Insets to Custom, with a value of 2 for each one.
  3. Set the Radius to Custom, with a value of 8.
  4. Select the Blue background color from the color picker (which is the same blue as returned by UIColor.blueColor() in code), as shown here:
    Interface
  5. Copy and paste the Start Stop Group in the Organizer, and connect the new Group to @IBOutlet var addPlotGroup in the code.
  6. Set the newly named Add Plots Group's Vertical Alignment to Bottom.
  7. Copy and paste the Add Plots Group, and connect this new pasted group to @IBOutlet var showPlotsGroup in the code.
  8. Add a Button object to the Start Stop Group, connect it to @IBOutlet var startStopButton in the code, change its Title to Start Plotting, and its background color to Black.
  9. Add another connection to @IBAction func startStopButtonPressed in the code.
  10. Add a Button object to the Add Plot Group with the same property values, except for the Title, which should be set to Add Current Coordinates, and connect it to to @IBOutlet var addPlotButton in the code.
  11. Add another connection to @IBAction func addPlotButtonPressed in the code.
  12. Add a Button object to the Show Plots Group, with the same property values, except for the Title, which we change to Show Plots, and connect it to @IBOutlet var showPlotsButton in the code.
  13. Add another connection to @IBAction func showPlotsButtonPressed in the code.
  14. Drag a Label object onto the interface, set its Vertical Alignment to Center, its number of Lines to 0, and its Font property to Footnote.
  15. Connect the label to @IBOutlet var infoLable in the code.

Your Interface Builder window should now look like this:

Interface

Test your code

To run the code we simply need to comment out the sendPlotsButtonTapped implementation by enclosing it between /* and */, like so:

    @IBAction func sendPlotsButtonTapped() {
/*    WatchConnectivityManager.sharedManager.sendDataToWatch(locationManager.currentLocations)
*/
    }

Now the code will compile, since we no longer have a reference to the WatchConnectivityManager that we haven't coded yet.

Hit Run, and when your app has launched, tap the Start Plotting button. Add a location by tapping Add Current Coordinates and your screen will, after a few seconds, look like the illustration here:

Test your code

If you tap Stop Plotting, the Show Plots button will become visible, but tapping it will do nothing until we have added the PlotScene controller.

We will do this next.

PlotsSceneInterfaceController

The PlotSceneInterfaceController that we are about to create will be responsible for accepting a list of CLLocation objects, that is, the LocationSet type, and presenting it in a WKInterfaceTable. The code here is very straightforward and will present no new challenges.

CodingPlotsSceneInterfaceController

Create a new Swift file and name it PlotsSceneInterfaceController.swift. Replace the import statement with the following code:

Import WatchKit

Class TableRowController: NSObject {
    @IBOutlet var label: WKInterfaceLabel!
}

Class PlotsSceneInterfaceController: WKInterfaceController {
}

We have chosen to define the TableRowController class that we will need for out table in the same file as the PlotsSceneInterfaceController. We would probably create a dedicated file for this class if its logic were more complex, but in this case we only need to define a single IBOutlet property and so we include it here.

Now to add the code for the PlotsSceneInterfaceController class.

Add the following code to the class:

Class PlotsSceneInterfaceController: WKInterfaceController {

    @IBOutlet var plotsTable: WKInterfaceTable!

    Override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        if let data = context as? LocationSet{ //1
            loadTable(data)
        }
    }
    func loadTable(data: LocationSet) {
        plotsTable.setNumberOfRows(data.count, withRowType: "TableRowControllerID")
        for (index, location) in data.enumerate() {
            if let row = plotsTable.rowControllerAtIndex(index) as? TableRowController {
                row.label.setText( //2
                    "Location (index + 1)
" + //3
                    "Lat: (location.coordinate.latitude)
" + //4
                    "Lon: (location.coordinate.longitude)"
                )
            }
        }
    }
}

The comments in the code are as follows:

  1. We check that the context data is of the correct type and, if it is, we pass it directly to our loadTable method.
  2. The only thing in this code that we have not seen before is the way the text of row.label is spread over several lines for readability. Swift will not allow a single String to be defined across more than one line, so it is necessary to use the + operator to concatenate the individual strings. However, there is nothing to stop you writing the entire string within one set of quotation marks.
  3. We display to the user the number (i.e. index) of the location in the row, but since most people start counting at 1, we add 1 to the index. There are some interesting articles on the web that argue that children should be taught to start counting at zero. Totally beyond our scope, but worth a search. And if it catches on, we'll come back and update the app!
  4. In this case, we choose only to use the lat and lon properties of the CLLocation object. You may like to investigate adding others once this chapter is complete.

Creating the UI

Possibly the most error-prone part of using WKInterfaceTable objects is hooking them up to the UI in Interface Builder, so, although we're doing nothing new here, we will go through it step by step:

  1. Drag an InterfaceController object into the Interface Builder window and, in the Identity Inspector, set its Class to be PlotsSceneInterfaceController.
  2. In the Attributes Inspector, set the Identifier to PlotsScene. If you check back in the InterfaceController class code, this is the identifier used in the showPlotsButtonTapped method.

    Tip

    Copying and pasting is the safest way to avoid the frustration caused by typing-errors between Interface Builder's identifiers and the source code, since IB cannot make use of the String constants we define in Xcode.

  3. Drag a Table object onto the PlotsScene interface and connect it to @IBOut let var plotsTable in the PlotsSceneInterfaceController class's source code.
  4. Select Table Row Controller in the Organizer pane and set its Class to TableRowController in the Identity Inspector.
  5. In the Attributes Inspector, set Identifier to TableRowControllerID. You might like to copy and paste this from the PlotsSceneInterfaceController class's loadTable method.
  6. Select the table row controller's Group object and set its Height property to Size To Fit Content.
  7. Drag a Label object into the table row controller's Group object and connect it to @IBOutlet var label in the TableRowController class's source code.
  8. Set the label's Font property to Caption 2, its Min Scale property to 0.6, its number of Lines to 0, and its Width property to Relative To Container.

Sometimes it can seem a little confusing having to hook up all these Identifier and Class values in Interface Builder, but, like most things, the trick is to take it slowly at first, double checking that you're setting the right IB property with the right name.

Run the app

Now when you hit Run, you will be able to see the second screen displaying all the locations you have added on the first. We're almost done and the only thing the watch app needs is the class that will send data to the iPhone.

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

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