Search code examples
iosswiftmapkitwatchkitapple-watch

Apple Watch app: run an MKDirectionsRequest through the parent iOS app in the background?


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:

Location services are "Always" enabled for my app

So the question now becomes: is there any way to "force" an MKDirectionsRequest to happen in the background?


Solution

  • 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()
        // ...
    }