[英]Apple Watch app: run an MKDirectionsRequest through the parent iOS app in the background?
我正在编写Apple Watch应用,有时需要获取有关从用户当前位置到特定位置的步行或驾车距离的信息。
正如Apple在其《 Apple Watch编程指南》中所建议的那样,我将所有辛苦工作委托给iOS应用程序,方法是从Apple Watch调用openParentApplication
并在iOS应用程序端实现handleWatchKitExtensionRequest
函数。 因此,iOS应用程序负责:1)使用MapKit计算到目的地的路线,以及2)将获取的距离和预期时间返回给Apple Watch。
该操作是通过MapKit的MKDirectionsRequest
,该操作往往很“慢”(例如1或2秒)。 如果我使用相同的参数直接在iOS应用中测试代码,那么一切将正常运行:我得到了预期的时间和距离响应。 但是,从Apple Watch应用程序内部,永远不会调用回调( openParentApplication
reply
参数),并且设备永远也不会获取其信息。
更新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
在后台发生?
此代码可在我正在使用的应用程序中使用。 它也可以在后台与该应用程序一起使用,因此我可以肯定地说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()
// ...
}
您的完成处理程序有一个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 ...]"
因此,我们可能不得不等待下一个Beta。
在您提供给我们的代码摘录中(虽然您没有明确指出,但我猜想是来自handleWatchKitExtensionRequest
),您不会在openParentApplication
调用传递给iPhone应用程序的回复块。 对于具有此问题的其他开发人员,这是在这些情况下应检查的第一件事。
但是,第二次更新表明,当iPhone应用程序位于前台时,它可以正常工作。 这几乎可以肯定地表明该问题是位置服务权限之一。 如果您的应用在运行时具有访问位置服务的权限,但没有“总是”权限,那么当您的iPhone应用未运行时,WatchKit Extension将无法从MapKit接收结果。 请求(和接收)这种许可应该可以解决您的问题。
对于更普遍的问题,即那些没有看到被调用的回复块的人,在Swift中定义了该方法,
optional func application(_ application: UIApplication!,
handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!,
reply reply: (([NSObject : AnyObject]!) -> Void)!)
因此,Reply为您提供了一个块,您可以在向其传递AnyObject的同时执行该块。 您必须返回某些内容,即使它是reply(nil)
,也将收到错误消息,“ iPhone App中的UIApplicationDelegate从未调用reply()...”。
在Objective-C中,定义了方法,
- (void)application:(UIApplication *)application
handleWatchKitExtensionRequest:(NSDictionary *)userInfo
reply:(void (^)(NSDictionary *replyInfo))reply
请注意,这里的replyInfo
必须是可序列化为属性列表文件的NSDictionary。 该词典的内容仍由您决定,您可以指定nil。
因此,有趣的是,这可能是一个很好的API示例,与使用Objective-C相比,使用Swift具有明显的优势,因为在Swift中,您显然可以简单地传递任何对象,而无需将许多对象序列化为NSData块,从而能够通过Objective-C中的NSDictionary传递它们。
我有一个类似的问题,以我为例,事实证明,返回的字典需要具有可序列化的数据。 如果尝试返回CLLocation
数据,则需要使用NSKeyedArchiver/NSKeyedUnarchiver
进行序列化或将其转换为NSString
然后再将其传递给reply()
。
感谢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 ]);
}
})
})
})
这是我们实现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;
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.