I'm writing an Apple Watch app and at some point I need to get an information about the walking or driving distance from the user's current location to a specific place.
As recommended by Apple in its Apple Watch Programming Guide, I'm delegating all the hard work to the iOS app, by calling openParentApplication
from the Apple Watch and implementing the handleWatchKitExtensionRequest
function on the iOS app side. So, the iOS app is in charge of: 1) computing directions to the destination place using MapKit, and 2) returning the fetched distance and expected time to the Apple Watch.
This operation is made through MapKit's MKDirectionsRequest
, which tends to be "slow" (like, 1 or 2 seconds). If I test my code directly in the iOS app with the same arguments, everything works well: I get the expected time and distance response. However, from inside the Apple Watch app, the callback (reply
parameter of openParentApplication
) is never called and the device never gets its information back.
UPDATE 1: Replaced by update 3.
UPDATE 2: Actually, there is no timeout as I suspected in the beginning, but it only seems to work if the iOS app runs in foreground on the iPhone. If I try to run the query from the Apple Watch app without touching anything on the iPhone Simulator (i.e.: the app is woken up in the background), then nothing happens. As soon as I tap my app's icon on the iPhone Simulator, putting it frontmost, the Apple Watch receives the reply.
UPDATE 3: As requested by Duncan, below is the full code involved, with emphasis on where execution path is lost:
(in class WatchHelper
)
var callback: (([NSObject : AnyObject]!) -> Void)?
func handleWatchKitExtensionRequest(userInfo: [NSObject : AnyObject]!, reply: (([NSObject : AnyObject]!) -> Void)!) {
// Create results and callback object for this request
results = [NSObject: AnyObject]()
callback = reply
// Process request
if let op = userInfo["op"] as String? {
switch op {
case AppHelper.getStationDistanceOp:
if let uic = userInfo["uic"] as Int? {
if let transitType = userInfo["transit_type"] as Int? {
let transportType: MKDirectionsTransportType = ((transitType == WTTripTransitType.Car.rawValue) ? .Automobile : .Walking)
if let loc = DatabaseHelper.getStationLocationFromUIC(uic) {
// The following API call is asynchronous, so results and reply contexts have to be saved to allow the callback to get called later
LocationHelper.sharedInstance.delegate = self
LocationHelper.sharedInstance.routeFromCurrentLocationToLocation(loc, withTransportType: transportType)
}
}
}
case ... // Other switch cases here
default:
NSLog("Invalid operation specified: \(op)")
}
} else {
NSLog("No operation specified")
}
}
func didReceiveRouteToStation(distance: CLLocationDistance, expectedTime: NSTimeInterval) {
// Route information has been been received, archive it and notify caller
results!["results"] = ["distance": distance, "expectedTime": expectedTime]
// Invoke the callback function with the received results
callback!(results)
}
(in class LocationHelper
)
func routeFromCurrentLocationToLocation(destination: CLLocation, withTransportType transportType: MKDirectionsTransportType) {
// Calculate directions using MapKit
let currentLocation = MKMapItem.mapItemForCurrentLocation()
var request = MKDirectionsRequest()
request.setSource(currentLocation)
request.setDestination(MKMapItem(placemark: MKPlacemark(coordinate: destination.coordinate, addressDictionary: nil)))
request.requestsAlternateRoutes = false
request.transportType = transportType
let directions = MKDirections(request: request)
directions.calculateDirectionsWithCompletionHandler({ (response, error) -> Void in
// This is the MapKit directions calculation completion handler
// Problem is: execution never reaches this completion block when called from the Apple Watch app
if response != nil {
if response.routes.count > 0 {
self.delegate?.didReceiveRouteToStation?(response.routes[0].distance, expectedTime: response.routes[0].expectedTravelTime)
}
}
})
}
UPDATE 4: The iOS app is clearly setup to be able to receive location updates in the background, as seen in the screenshot below:
So the question now becomes: is there any way to "force" an MKDirectionsRequest
to happen in the background?
This code works in an app that I am working on. It also works with the app in the background so I think it's safe to say that MKDirectionsRequest
will work in background mode. Also, this is called from the AppDelegate and is wrapped in a beginBackgroundTaskWithName
tag.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
MKPlacemark *destPlacemark = [[MKPlacemark alloc] initWithCoordinate:CLLocationCoordinate2DMake(destLat, destLon) addressDictionary:nil];
MKPlacemark *currentPlacemark = [[MKPlacemark alloc] initWithCoordinate:CLLocationCoordinate2DMake(currLat, currLon) addressDictionary:nil];
NSMutableDictionary __block *routeDict=[NSMutableDictionary dictionary];
MKRoute __block *routeDetails=nil;
MKDirectionsRequest *directionsRequest = [[MKDirectionsRequest alloc] init];
[directionsRequest setSource:[[MKMapItem alloc] initWithPlacemark:currentPlacemark]];
[directionsRequest setDestination:[[MKMapItem alloc] initWithPlacemark:destPlacemark]];
directionsRequest.transportType = MKDirectionsTransportTypeAutomobile;
dispatch_async(dispatch_get_main_queue(), ^(){
MKDirections *directions = [[MKDirections alloc] initWithRequest:directionsRequest];
[directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) {
if (error) {
NSLog(@"Error %@", error.description);
} else {
NSLog(@"ROUTE: %@",response.routes.firstObject);
routeDetails = response.routes.firstObject;
[routeDict setObject:[NSString stringWithFormat:@"%f",routeDetails.distance] forKey:@"routeDistance"];
[routeDict setObject:[NSString stringWithFormat:@"%f",routeDetails.expectedTravelTime] forKey:@"routeTravelTime"];
NSLog(@"Return Dictionary: %@",routeDict);
reply(routeDict);
}
}];
});
});
EDIT from OP: The code above probably works in ObjC, but the exact reason why it works is that it is not using MKMapItem.mapItemForCurrentLocation()
. So the working code for me looks as follows:
func routeFromCurrentLocationToLocation(destination: CLLocation, withTransportType transportType: MKDirectionsTransportType) {
// Calculate directions using MapKit
let currentLocation = MKMapItem(placemark: MKPlacemark(coordinate: CLLocationCoordinate2DMake(lat, lng), addressDictionary: nil))
var request = MKDirectionsRequest()
// ...
}