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.
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:
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.awakeWithContext
method, by which we can be sure that locationManager
never equals nil
. If it did, the app would crash.locationManager
to request the current location.locationManager
receives its data, it will call this method on its delegate. For the moment we just print the results to the console for debuggingHave 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.
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.
Your Watch Simulator or your device should now ask you for permission to access your location data, as pictured here:
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:
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.
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.
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.
Now we must return to Interface Builder to create the user interface.
Interface.storyboard
file, and ensure that the Assistant Editor is open (Command-Option-Return).@IBAction func sendPlotsButtonTapped()
in the source code in the Assistant Editor.Now we must add the Group objects to the interface, given as follows.
@IBOutlet
var startStopGroup
in the source code.UIColor.blueColor()
in code), as shown here:@IBOutlet var addPlotGroup
in the code.@IBOutlet var showPlotsGroup
in the code.@IBOutlet var startStopButton
in the code, change its Title to Start Plotting, and its background color to Black.@IBAction func startStopButtonPressed
in the code.@IBOutlet
var
addPlotButton
in the code.@IBAction func addPlotButtonPressed
in the code.@IBOutlet var showPlotsButton
in the code.@IBAction func showPlotsButtonPressed
in the code. @IBOutlet var infoLable
in the code.Your Interface Builder window should now look like this:
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:
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.
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.
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:
context
data is of the correct type and, if it is, we pass it directly to our loadTable
method.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.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!lat
and lon
properties of the CLLocation
object. You may like to investigate adding others once this chapter is complete.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:
InterfaceController
class code, this is the identifier used in the showPlotsButtonTapped
method.@IBOut let var plotsTable
in the PlotsSceneInterfaceController
class's source code.PlotsSceneInterfaceController
class's loadTable
method.@IBOutlet var label
in the TableRowController
class's source code.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.
3.144.115.154