Apple Watch应用程序:在后台通过父iOS应用程序运行MKDirectionsRequest?

时间:2015-01-02 22:49:39

标签: ios swift mapkit watchkit apple-watch

我正在编写Apple Watch应用程序,在某些时候我需要获取有关从用户当前位置到特定位置的步行或行车距离的信息。

正如Apple在其Apple Watch Programming Guide中所建议的那样,我通过从Apple Watch调用openParentApplication并在其上实施handleWatchKitExtensionRequest功能,将所有辛苦工作委托给iOS应用程序。 iOS应用程序端。因此,iOS应用程序负责:1)使用MapKit计算到目的地的路线,以及2)将获取的距离和预期时间返回给Apple Watch。

此操作是通过MapKit的MKDirectionsRequest进行的,该reply往往是“慢”(如1或2秒)。如果我直接在iOS应用程序中使用相同的参数测试我的代码,一切都运行良好:我得到预期的时间和距离响应。但是,从Apple Watch应用程序内部,永远不会调用回调(openParentApplication的{​​{1}}参数),并且设备永远不会获取其信息。

更新1:由更新3替换。

更新2:实际上,我没有在开始时怀疑超时,但只有当iOS应用程序在iPhone上运行时,它似乎才有效。如果我尝试从Apple Watch应用程序运行查询而不触及iPhone模拟器上的任何内容(即:应用程序在后台唤醒),则没有任何反应。只要我点击iPhone模拟器上的应用程序图标,将其放在最前面,Apple Watch就会收到回复。

更新3:根据Duncan的要求,下面是相关的完整代码,重点介绍执行路径丢失的位置:

(在班级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)
}

(在班级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)
            }
        }
    })
}

更新4 :iOS应用程序设置清晰,可以在后台接收位置更新,如下面的屏幕截图所示:

Location services are "Always" enabled for my app

所以问题现在变成:有没有办法“强迫”MKDirectionsRequest在后​​台发生?

5 个答案:

答案 0 :(得分:0)

您的完成处理程序有一个error对象,您应该检查传入的内容。

openParentApplicationhandleWatchKitExtensionRequest在Xcode 6.2 Beta 2中运行良好,似乎在Xcode 6.2 Beta 3(6C101)中被破坏了。我总是得到错误

Error Domain=com.apple.watchkit.errors Code=2 
"The UIApplicationDelegate in the iPhone App never called reply() in -[UIApplicationDelegate ...]"

所以我们可能要等下一个测试版。

答案 1 :(得分:0)

在您提供给我们的代码提取中(我假设它来自handleWatchKitExtensionRequest,虽然您没有明确指出),但您不会调用{{1}中传递给您的iPhone应用程序的回复块1}}。对于遇到此问题的其他开发人员而言,这是应该在这些方案中首先检查的。

但是,您的第二次更新表明当iPhone应用程序位于前台时它正常工作。这几乎肯定表明该问题是位置服务权限之一。如果您的应用程序在运行时有权访问位置服务,但没有“始终”权限,则当您的iPhone应用程序未运行时,您的WatchKit扩展程序将无法从MapKit接收结果。请求(和接收)此类权限应解决您的问题。

对于那些没有看到调用回复块的问题的人来说,在Swift中定义了这个方法,

openParentApplication

因此,Reply会为您提供一个块,在传递AnyObject时执行该块。您必须返回一些内容,即使它是optional func application(_ application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!, reply reply: (([NSObject : AnyObject]!) -> Void)!) ,否则您将收到错误消息“iPhone应用程序中的UIApplicationDelegate从未调用reply()......”

在Objective-C中,定义了方法,

reply(nil)

请注意,- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *replyInfo))reply 必须是可序列化为属性列表文件的NSDictionary。此词典的内容仍由您自行决定,您可以指定nil。

有趣的是,这可能是API的一个很好的例子,其中使用Swift优于Objective-C有明显的优势,因为在Swift中你显然可以简单地传递任何对象,而无需将许多对象序列化为NSData块为了能够通过Objective-C中的NSDictionary传递它们。

答案 2 :(得分:0)

我有一个类似的问题,在我的情况下,事实证明返回的字典需要有可以序列化的数据。如果您要返回CLLocation数据,则需要先使用NSKeyedArchiver/NSKeyedUnarchiver对其进行序列化,或将其转换为NSString,然后再将其传递给reply()

答案 3 :(得分:0)

感谢Romain,您的代码节省了我的一天。我刚刚转换为Swift

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in


            let destPlacemark = MKPlacemark(coordinate: coordinateDestinazione, addressDictionary: nil)

            let miaLocation = self.Manager.location.coordinate
            let currentPlacemark = MKPlacemark(coordinate: miaLocation, addressDictionary: nil)



            var routeDetails = MKRoute()
            let directionRequest = MKDirectionsRequest()
            directionRequest.setSource(MKMapItem(placemark: currentPlacemark))
            directionRequest.setDestination(MKMapItem(placemark: destPlacemark))
            directionRequest.transportType = MKDirectionsTransportType.Automobile


            dispatch_async(dispatch_get_main_queue(), { () -> Void in


                let directions = MKDirections(request: directionRequest)

                directions.calculateDirectionsWithCompletionHandler({ (
                    response, error) -> Void in

                    if (error != nil) {
                        NSLog("Error %@", error.description);

                    } else {
                        println("Route: \(response.routes.first)")
                        routeDetails = response.routes.first as! MKRoute



                        reply(["Distance" : routeDetails.distance, "TravelTime" : routeDetails.expectedTravelTime ]);
                    }
                })
            })
            })

答案 4 :(得分:-1)

以下是我们实施beginBackgroundTaskWithName

的方式
-(void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *replyInfo))reply{


Model *model=[Model sharedModel];

UIApplication *app = [UIApplication sharedApplication];

UIBackgroundTaskIdentifier bgTask __block = [app beginBackgroundTaskWithName:@"watchAppRequest" expirationHandler:^{

    NSLog(@"Background handler called. Background tasks expirationHandler called.");
    [[UIApplication sharedApplication] endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;

}];

//create an empty reply dictionary to be used later
NSDictionary *replyInfo __block=[NSDictionary dictionary];

//get the dictionary of info sent from watch
NSString *requestString=[userInfo objectForKey:@"request"];

//get the WatchAppHelper class (custom class with misc. functions)
WatchAppHelper *watchAppHelper=[WatchAppHelper sharedInstance];


//make sure user is logged in
if (![watchAppHelper isUserLoggedIn]) {

    //more code here to get user location and misc. inf.

    //send reply back to watch
    reply(replyInfo);

}


[app endBackgroundTask:bgTask];
bgTask=UIBackgroundTaskInvalid;

}