1

我正在编写一个 Apple Watch 应用程序,有时我需要获取有关从用户当前位置到特定地点的步行或驾驶距离的信息。

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

这个操作是通过 MapKit 进行的MKDirectionsRequest,它往往是“慢”的(比如 1 或 2 秒)。如果我直接在 iOS 应用程序中使用相同的参数测试我的代码,那么一切正常:我得到了预期的时间和距离响应。但是,在 Apple Watch 应用程序内部,回调(reply的参数openParentApplication)永远不会被调用,并且设备永远不会取回它的信息。

更新 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 应用程序显然设置为能够在后台接收位置更新,如下面的屏幕截图所示:

定位服务是

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

4

6 回答 6

2

此代码适用于我正在开发的应用程序。它也可以在后台与应用程序一起使用,所以我认为可以肯定地说MKDirectionsRequest它将在后台模式下工作。此外,这是从 AppDelegate 调用的,并被包装在beginBackgroundTaskWithName标签中。

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);
                            }
                        }];

                    });
            });

从 OP 编辑​​:上面的代码可能在 ObjC 中工作,但它工作的确切原因是它没有使用MKMapItem.mapItemForCurrentLocation(). 所以我的工作代码如下所示:

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()
    // ...
}
于 2015-05-01T19:16:46.763 回答
0

我有一个类似的问题,就我而言,事实证明返回的字典需要具有可以序列化的数据。如果您尝试返回CLLocation数据,则需要在NSKeyedArchiver/NSKeyedUnarchiver将其NSString传递给reply().

于 2015-04-16T20:22:20.120 回答
0

在您提供给我们的代码摘录中(我认为它来自handleWatchKitExtensionRequest,尽管您没有特别指出),您没有调用在 中传递给您的 iPhone 应用程序的回复块openParentApplication。对于有此问题的其他开发人员,这是在这些场景中应该检查的第一件事。

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

对于更普遍地遇到看不到被调用的回复块的问题的人,在 Swift 中定义了该方法,

optional func application(_ application: UIApplication!, 
handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!, 
reply reply: (([NSObject : AnyObject]!) -> Void)!)

因此,Reply 为您提供了一个块,您在传递 AnyObject 时执行该块。您必须返回某些内容,即使它是reply(nil),否则您将收到错误消息“iPhone 应用程序中的 UIApplicationDelegate 从未调用回复()...”

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

- (void)application:(UIApplication *)application
handleWatchKitExtensionRequest:(NSDictionary *)userInfo
reply:(void (^)(NSDictionary *replyInfo))reply

请注意,此处replyInfo必须是可序列化为属性列表文件的 NSDictionary。本词典的内容仍由您自行决定,您可以指定 nil。

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

于 2015-01-11T03:00:38.120 回答
0

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

openParentApplication并且handleWatchKitExtensionRequest在 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 ...]"

所以我们可能不得不等待下一个测试版。

于 2015-01-03T08:49:28.047 回答
0

谢谢罗曼,你的代码拯救了我的一天。我刚刚转换为 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 ]);
                    }
                })
            })
            })
于 2015-05-29T12:53:07.100 回答
-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;

}

于 2015-04-16T23:39:36.217 回答