A WatchKit Map Tutorial - WatchKit App Development Essentials – First Edition (2015)

WatchKit App Development Essentials – First Edition (2015)

26. A WatchKit Map Tutorial

WatchKit currently provides limited support for displaying maps within an app running on an Apple Watch device. The features offered by the WKInterfaceMap class consist of the ability to display a designated map region and to add annotations in the form of colored pins or custom images at specified locations within the defined region. When tapped by the user, the Map object opens the built-in Apple Watch Map app configured to display the same region.

The remainder of this chapter will work through a basic tutorial designed to highlight some of the key features of the WKInterfaceMap class. The project will demonstrate the use of the parent iOS app to obtain current location information in the background and use it to display the current location within a WatchKit app.

26.1 Creating the Example Map Project

Start Xcode and create a new iOS project. On the template screen choose the Application option located under iOS in the left hand panel and select Single View Application. Click Next, set the product name to MapDemoApp, enter your organization identifier and make sure that the Devicesmenu is set to Universal. Before clicking Next, change the Language menu to Swift. On the final screen, choose a location in which to store the project files and click on Create to proceed to the main Xcode project window.

26.2 Adding the WatchKit App Target to the Project

Within Xcode, select the File -> New -> Target… menu option. In the target template dialog, select the Apple Watch option listed beneath the iOS heading. In the main panel, select the WatchKit App icon and click on Next. On the subsequent screen turn off the Include Glance Scene andInclude Notification Scene options before clicking on the Finish button.

As soon as the extension target has been created, a new panel will appear requesting permission to activate the new scheme for the extension target. Activate this scheme now by clicking on the Activate button in the request panel.

26.3 Designing the WatchKit App User Interface

Select the Interface.storyboard file and drag and drop a Map and Slider object from the Object Library onto the scene canvas so that the layout matches that shown in Figure 26-1:

Figure 26-1

Select the Slider object, display the Attributes Inspector and configure the following property values:

· Value: 1

· Minimum: 1

· Maximum: 10

· Steps: 10

Display the Assistant Editor and establish an outlet connection from the Map object named mapObject. With the Assistant Editor still displayed, establish an action connection from the Slider object to a method named changeMapRegion.

26.4 Configuring the Containing iOS App

The rules of WatchKit app development dictate that tasks such as obtaining location information should be performed by the containing iOS app. In the case of this example, the containing iOS app will be required to identify the current location of the user and return that data to the WatchKit app extension. Before implementing this behavior, however, the iOS app target needs to be configured to enable access to location information.

Before any application can begin to track location information when running in the background it must first seek permission to do so from the user. This can be achieved by making a call to the requestAlwaysAuthorization method of the CLLocationManager instance. Since the iOS app will be launched in the background by the WatchKit app, it is essential that this method call be made within the iOS app.

Select the AppDelegate.swift file for the iOS app target and modify the code to import the CoreLocation framework, store a reference to the CLLocationManager instance and call the requestAlwaysAuthorization method within the didFinishLaunchingWithOptions method:

import UIKit

import CoreLocation

@UIApplicationMain

class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?

let locationManager = CLLocationManager()

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

locationManager.requestAlwaysAuthorization()

return true

}

.

.

}

The requestAlwaysAuthorization method call requires that a specific key-value pair be added to the Information Property List dictionary contained within the application’s Info.plist file. The value takes the form of a string describing the reason why the application needs access to the user’s current location. In the case of background access to location information, the NSLocationAlwaysUsageDescription key must be added to the property list.

Within the Project Navigator panel, load the Info.plist file (located under the Supporting Files section of the MapDemoApp iOS app target) into the editor. The key-value pair needs to be added to the Information Property List dictionary. Select this entry in the list and click on the + button to add a new entry to the dictionary. Within the new entry, enter NSLocationAlwaysUsageDescription into the key column and, once the key has been added, double-click in the corresponding value column and enter the following text:

This information is required to identify your current location

Once the entry has been added to the Info.plist file it should appear as illustrated in Figure 26-2:

Figure 26-2

The iOS app must now be run and the location tracking request approved. Select MapDemoApp in the Xcode run target menu and launch the iOS app on a device or simulator session. The first time that the app is launched, the system will request permission to track location information in the background (Figure 26-3). Tap the Allow button to enable this access.

Figure 26-3

26.5 Enabling Background Location Updates

When it is opened by the WatchKit app, the containing iOS app will be launched in the background. The containing iOS app also needs to be configured, therefore, to allow background location updates to be received. To enable this mode, select the MapDemoApp target located at the top of the Project Navigator panel and make sure that the target menu in the settings panel is set to MapDemoApp as indicated in Figure 26-4:

Figure 26-4

Select the Capabilities tab and enable the Background Modes option. Once enabled, activate support for Location updates by selecting the corresponding checkbox:

Figure 26-5

26.6 Handling the Open Parent App Request

The steps outlined in the chapter entitled WatchKit App and Parent iOS App Communication will now be used to launch the parent iOS app, obtain the user’s current location and return that information to the WatchKit app extension.

The first step in this process is to implement the code in the app delegate of the iOS app to handle the launch request. Select the AppDelegate.swift file and begin by adding the handleWatchKitExtensionRequest method and declaring a variable in which to store the background request identifier value:

import UIKit

import CoreLocation

@UIApplicationMain

class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?

let locationManager = CLLocationManager()

var bgIdentifier: UIBackgroundTaskIdentifier?

func application(application: UIApplication,

handleWatchKitExtensionRequest userInfo:

[NSObject : AnyObject]?,

reply: (([NSObject : AnyObject]!) -> Void)!) {

bgIdentifier = application.beginBackgroundTaskWithName(

"MyTask", expirationHandler: { () -> Void in

println("Time expired")

})

}

.

.

}

As discussed in the WatchKit App and Parent iOS App Communication chapter, one of the arguments passed through to the handleWatchKitExtensionRequest is a reference to the reply handler closure that is to be called in order to return data to the WatchKit extension. The mechanism for obtaining location data in iOS operates in a way that will require us to call the reply handler closure from within another method of the app delegate class. To make this possible, it will be necessary to store a reference to the closure handler reference in a variable where it can be accessed by other methods in the class. Remaining in the AppDelegate.swift file, therefore, add a variable configured with the signature of the closure and a line of code to store the reference as follows:

import UIKit

import CoreLocation

@UIApplicationMain

class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?

let locationManager = CLLocationManager()

var bgIdentifier: UIBackgroundTaskIdentifier?

var replyHandler:([NSObject : AnyObject]!)->Void = {arg in}

func application(application: UIApplication,

handleWatchKitExtensionRequest userInfo:

[NSObject : AnyObject]?,

reply: (([NSObject : AnyObject]!) -> Void)!) {

replyHandler = reply

bgIdentifier = application.beginBackgroundTaskWithName(

"MyTask", expirationHandler: { () -> Void in

println("Time expired")

})

}

.

.

}

26.7 Getting the Current Location

The next step is to instruct the location manager to begin receiving location updates. This involves declaring the AppDelegate class as implementing the CLLocationManagerDelegate protocol, assigning the class as the delegate for the location manager and starting the location update process:

.

.

.

class AppDelegate: UIResponder, UIApplicationDelegate, CLLocationManagerDelegate {

var window: UIWindow?

let locationManager = CLLocationManager()

var bgIdentifier: UIBackgroundTaskIdentifier?

var replyHandler:([NSObject : AnyObject]!)->Void = {arg in}

func application(application: UIApplication,

handleWatchKitExtensionRequest userInfo:

[NSObject : AnyObject]?,

reply: (([NSObject : AnyObject]!) -> Void)!) {

replyHandler = reply

bgIdentifier = application.beginBackgroundTaskWithName(

"MyTask", expirationHandler: { () -> Void in

println("Time expired")

})

locationManager.delegate = self

locationManager.startUpdatingLocation()

}

.

.

}

Each time the location manager receives a location update, it will call the didUpdateLocations method on the delegate. This class now needs to be implemented in the AppDelegate.swift file as follows:

func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {

locationManager.stopUpdatingLocation()

let currentLocation = locations[locations.count - 1]

as? CLLocation

var replyValues = Dictionary<String, AnyObject>()

replyValues["latitude"] = currentLocation?.coordinate.latitude

replyValues["longitude"] = currentLocation?.coordinate.longitude

UIApplication.sharedApplication().endBackgroundTask(bgIdentifier!)

replyHandler(replyValues)

}

The code begins by stopping location updates and accessing the most recent location data from the array of locations passed to the method. A Dictionary object is then created and entries added for the latitude and longitude of the current location. The system is then notified that the background task is complete before the reply handler is called via the previously declared reference variable passing through the dictionary containing the location data.

Work on the parent iOS app is now complete. All that remains is to add some code to the WatchKit extension to launch the parent app and display and manage the Map object based on the location data and the Slider object settings.

26.8 Implementing the WatchKit Extension Map Code

With the parent iOS app now adapted to handle an open request, the next step is to make a call to the openParentApplication method in the main interface controller class of the WatchKit app extension. Locate and select the InterfaceController.swift file and modify the awakeWithContextmethod to include a call to the openParentApplication method:

override func awakeWithContext(context: AnyObject?) {

super.awakeWithContext(context)

let parentValues = ["task" : "getLocation"]

WKInterfaceController.openParentApplication(parentValues, reply: { (replyValues, error) -> Void in

})

}

Next, code needs to be added to the reply closure to extract the location data returned from the parent app and use it to configure the map object. This also requires the addition of a variable in which to store the user’s current location information:

class InterfaceController: WKInterfaceController {

@IBOutlet weak var mapObject: WKInterfaceMap!

var mapLocation: CLLocationCoordinate2D?

override func awakeWithContext(context: AnyObject?) {

super.awakeWithContext(context)

let parentValues = ["task" : "getLocation"]

WKInterfaceController.openParentApplication(parentValues, reply: { (replyValues, error) -> Void in

let lat = replyValues["latitude"] as! Double

let long = replyValues["longitude"] as! Double

self.mapLocation = CLLocationCoordinate2DMake(lat, long)

let span = MKCoordinateSpanMake(0.1, 0.1)

let region = MKCoordinateRegionMake(self.mapLocation!,

span)

self.mapObject.setRegion(region)

self.mapObject.addAnnotation(self.mapLocation!,

withPinColor: .Red)

})

}

.

.

}

The first task performed by the reply handler closure is to extract the latitude and longitude values from the reply dictionary object:

let lat = replyValues["latitude"] as! Double

let long = replyValues["longitude"] as! Double

These values are then used to create a CLLocationCoordinate2D object which is stored in the mapLocation variable:

self.mapLocation = CLLocationCoordinate2DMake(lat, long)

Next a span value is defined to dictate the area that will be covered by the map region. This is then used along with the current location to create the region that will be displayed by the map:

let span = MKCoordinateSpanMake(0.1, 0.1)

let region = MKCoordinateRegionMake(self.mapLocation!, span)

Finally, the map object is configured to display the region and a red pin added to mark the current location:

self.mapObject.setRegion(region)

self.mapObject.addAnnotation(self.mapLocation!, withPinColor: .Red)

The color of the pin is specified using the WKInterfaceMapPinColor constant which provides the following color options:

· WKInterfaceMapPinColor.Red

· WKInterfaceMapPinColor.Green

· WKInterfaceMapPinColor.Purple

With these changes made to the interface controller class, compile and run the WatchKit app which, when running, should display a map region centered around the user’s current location as shown in Figure 26-6:

Figure 26-6

When running in the simulator, the location will be based on the current setting of the Debug -> Location menu.

26.9 Adding Zooming Support

A zooming effect can be added to a map by enlarging and reducing the currently displayed region. For the purposes of this example, the current region will be modified by the Slider object. Previously in this chapter the Slider object in the WatchKit app scene was connected to an action method named changeMapRegion. Edit the InterfaceController.swift file, locate this method and modify it as follows to change the region span based on the current slider setting:

@IBAction func changeMapRegion(value: Float) {

let degrees:CLLocationDegrees = CLLocationDegrees(value) / 10

let span = MKCoordinateSpanMake(degrees, degrees)

let region = MKCoordinateRegionMake(mapLocation!, span)

mapObject.setRegion(region)

}

Run the WatchKit app again and check that changes to the Slider object are reflected in the currently displayed map region giving the effect of zooming in and out of the map.

26.10 Summary

Maps are represented in WatchKit by the WKInterfaceMap class. This class is limited to displaying a static map region together with optional annotation markers in the form of pins or custom images. Tapping on the map launches the built-in Apple Watch Map app configured to display the same region where a wider range of options are available to the user.

This chapter has worked through the creation of an example project intended to highlight the basic features of the WKInterfaceMap class and to demonstrate the use of the parent iOS app in obtaining location data on behalf of the WatchKit app.