Apple Watch 应用:运行 通过父 iOS 应用在后台发出 MKDirections 请求?
Apple Watch app: run an MKDirectionsRequest through the parent iOS app in the background?
我正在编写 Apple Watch 应用程序,有时我需要获取有关从用户当前位置到特定地点的步行或驾车距离的信息。
按照 Apple 在其 Apple Watch Programming Guide 中的建议,我通过从 Apple Watch 调用 openParentApplication
并实现 handleWatchKitExtensionRequest
函数在 iOS 应用端。因此,iOS 应用程序负责:1) 使用 MapKit 计算到目的地的路线,以及 2) 将获取的距离和预期时间返回到 Apple Watch。
这个操作是通过 MapKit 的 MKDirectionsRequest
完成的,它往往是 "slow"(比如,1 或 2 秒)。如果我直接在 iOS 应用程序中使用相同的参数测试我的代码,一切正常:我得到了预期的时间和距离响应。但是,从 Apple Watch 应用程序内部,回调(openParentApplication
的 reply
参数)永远不会被调用,设备也永远不会取回其信息。
更新 1: 由更新 3 替换。
更新 2: 实际上,我一开始怀疑没有超时,但似乎只有 iOS 应用 运行 才有效s 在 iPhone 的前景中。如果我尝试 运行 来自 Apple Watch 应用程序的查询而不触及 iPhone 模拟器上的任何内容(即:应用程序在后台被唤醒),那么什么也不会发生。只要我在 iPhone 模拟器上点击我的应用程序图标,将其放在最前面,Apple Watch 就会收到回复。
更新 3: 根据 Duncan 的要求,下面是完整的代码,重点是执行路径丢失的地方:
(在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)
}
(在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)
}
}
})
}
更新 4:iOS 应用显然设置为能够在后台接收位置更新,如下面的屏幕截图所示:
所以现在的问题是:有什么方法可以让 "force" 在后台发生 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 ...]"
所以我们可能不得不等待下一个测试版。
在您提供给我们的代码摘录中(我推测是来自 handleWatchKitExtensionRequest
,尽管您没有具体说明),您没有调用传递给 iPhone openParentApplication
中的应用程序。对于遇到此问题的其他开发人员,这是在这些情况下应该检查的第一件事。
但是,您的第二次更新表明当 iPhone 应用程序位于前台时它工作正常。这几乎可以肯定表明问题是位置服务权限之一。如果您的应用在 运行 时有权访问位置服务,但没有 "always" 权限,那么当您的 iPhone 应用时,您的 WatchKit 扩展将无法从 MapKit 接收结果不是 运行。请求(并获得)这样的许可应该可以解决您的问题。
对于那些有看不到正在调用的回复块问题的更普遍的人,在 Swift 中定义了该方法,
optional func application(_ application: UIApplication!,
handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!,
reply reply: (([NSObject : AnyObject]!) -> Void)!)
Reply 因此为您提供了一个块,您可以在将 AnyObject 传递给该块时执行该块。您必须 return 某些内容,即使它是 reply(nil)
,否则您将收到错误消息 "The UIApplicationDelegate in the iPhone App never called reply()..."
在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 传递它们
我有一个类似的问题,在我的例子中,被 returned 的字典需要有可以序列化的数据。如果您尝试 return CLLocation
数据,则需要使用 NSKeyedArchiver/NSKeyedUnarchiver
对其进行序列化或将其转换为 NSString
,然后再将其传递给 reply()
.
以下是我们的实施方式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;
}
此代码适用于我正在开发的应用程序。它还在后台与应用程序一起使用,所以我认为可以肯定地说 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()
// ...
}
谢谢罗曼,你的代码救了我一命。我刚刚转换为 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 ]);
}
})
})
})
我正在编写 Apple Watch 应用程序,有时我需要获取有关从用户当前位置到特定地点的步行或驾车距离的信息。
按照 Apple 在其 Apple Watch Programming Guide 中的建议,我通过从 Apple Watch 调用 openParentApplication
并实现 handleWatchKitExtensionRequest
函数在 iOS 应用端。因此,iOS 应用程序负责:1) 使用 MapKit 计算到目的地的路线,以及 2) 将获取的距离和预期时间返回到 Apple Watch。
这个操作是通过 MapKit 的 MKDirectionsRequest
完成的,它往往是 "slow"(比如,1 或 2 秒)。如果我直接在 iOS 应用程序中使用相同的参数测试我的代码,一切正常:我得到了预期的时间和距离响应。但是,从 Apple Watch 应用程序内部,回调(openParentApplication
的 reply
参数)永远不会被调用,设备也永远不会取回其信息。
更新 1: 由更新 3 替换。
更新 2: 实际上,我一开始怀疑没有超时,但似乎只有 iOS 应用 运行 才有效s 在 iPhone 的前景中。如果我尝试 运行 来自 Apple Watch 应用程序的查询而不触及 iPhone 模拟器上的任何内容(即:应用程序在后台被唤醒),那么什么也不会发生。只要我在 iPhone 模拟器上点击我的应用程序图标,将其放在最前面,Apple Watch 就会收到回复。
更新 3: 根据 Duncan 的要求,下面是完整的代码,重点是执行路径丢失的地方:
(在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)
}
(在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)
}
}
})
}
更新 4:iOS 应用显然设置为能够在后台接收位置更新,如下面的屏幕截图所示:
所以现在的问题是:有什么方法可以让 "force" 在后台发生 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 ...]"
所以我们可能不得不等待下一个测试版。
在您提供给我们的代码摘录中(我推测是来自 handleWatchKitExtensionRequest
,尽管您没有具体说明),您没有调用传递给 iPhone openParentApplication
中的应用程序。对于遇到此问题的其他开发人员,这是在这些情况下应该检查的第一件事。
但是,您的第二次更新表明当 iPhone 应用程序位于前台时它工作正常。这几乎可以肯定表明问题是位置服务权限之一。如果您的应用在 运行 时有权访问位置服务,但没有 "always" 权限,那么当您的 iPhone 应用时,您的 WatchKit 扩展将无法从 MapKit 接收结果不是 运行。请求(并获得)这样的许可应该可以解决您的问题。
对于那些有看不到正在调用的回复块问题的更普遍的人,在 Swift 中定义了该方法,
optional func application(_ application: UIApplication!,
handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!,
reply reply: (([NSObject : AnyObject]!) -> Void)!)
Reply 因此为您提供了一个块,您可以在将 AnyObject 传递给该块时执行该块。您必须 return 某些内容,即使它是 reply(nil)
,否则您将收到错误消息 "The UIApplicationDelegate in the iPhone App never called reply()..."
在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 传递它们我有一个类似的问题,在我的例子中,被 returned 的字典需要有可以序列化的数据。如果您尝试 return CLLocation
数据,则需要使用 NSKeyedArchiver/NSKeyedUnarchiver
对其进行序列化或将其转换为 NSString
,然后再将其传递给 reply()
.
以下是我们的实施方式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;
}
此代码适用于我正在开发的应用程序。它还在后台与应用程序一起使用,所以我认为可以肯定地说 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()
// ...
}
谢谢罗曼,你的代码救了我一命。我刚刚转换为 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 ]);
}
})
})
})