I know it's been discussed that you cannot currently (as of xcode 6.3) test localnotifications pushing to the watch, but you can simulate remotenotification by using an .apns file and a dev scheme.
People say they are similar, so it seems like a good idea to use some kind of handling code to pass the data to do what you'd like. So for example in an apns with the following:
{
"aps": {
"alert": {
"body": "bodyText",
"title": "titleText"
},
"category": "myCategory"
},
"WatchKit Simulator Actions": [],
"customKey": "custom value."
}
I have an alert with "body" and "title" in it.
Now, to handle it in the watch's wkInterfaceController, i do the following
override func didReceiveRemoteNotification(remoteNotification: [NSObject : AnyObject], withCompletion completionHandler: ((WKUserNotificationInterfaceType) -> Void)) {
if let remoteaps:NSDictionary = remoteNotification["aps"] as? NSDictionary{
if let remotealert:NSDictionary = remoteaps["alert"] as? NSDictionary{
handleNotification( remoteAlert );
}
}
completeHandler( .Custom )
}
func handleNotification( alert : AnyObject? ){
if let alert = alert, let remotetitle = alert["title"] as? String{
println( "didReceiveRemoteNotification::remoteNotification.alert \(remotetitle)" )
titleLabel.setText(remotetitle);
}
if let alert = alert, let remotebody = alert["body"] as? String{
//println( "didReceiveRemoteNotification::remoteNotification.alert \(remotetitle)" )
bodyLabel.setText(remotebody);
}
}
Where the handling of the alert is abstracted, but how do I access that alert from localnotification's handler? Is that data in userInfo? somewhere else? Will this work?
override func didReceiveLocalNotification(localNotification: UILocalNotification, withCompletion completionHandler: ((WKUserNotificationInterfaceType) -> Void)) {
handleNotification( localNotification.userInfo ); //is this the right way to pass this?
completionHandler(.Custom)
}
Also, how do I send out that local notification to match that? Here is the phone side code to set up the notification:
var reminderNotification:UILocalNotification = UILocalNotification();
reminderNotification.category = "myCategory"
reminderNotification.alertTitle = "TitleText"
reminderNotification.alertBody = "BodyText"
reminderNotification.fireDate = NSDate().dateByAddingTimeInterval(60) //notify in one minute
reminderNotification.timeZone = NSTimeZone.systemTimeZone()
UIApplication.sharedApplication().scheduleLocalNotification(reminderNotification)
This should work:
- (void)didReceiveLocalNotification:(UILocalNotification *)localNotification withCompletion:(void (^)(WKUserNotificationInterfaceType))completionHandler {
// This method is called when a local notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
NSString *reminderId = [localNotification.userInfo valueForKey:kAppLocalNotificationIdKey]; // This is a custom key
[self manageNotification:#{#"title":localNotification.alertTitle,
#"body":localNotification.alertBody}
withId:reminderId];
// After populating your dynamic notification interface call the completion block.
completionHandler(WKUserNotificationInterfaceTypeCustom);
}
- (void)didReceiveRemoteNotification:(NSDictionary *)remoteNotification withCompletion:(void (^)(WKUserNotificationInterfaceType))completionHandler {
// This method is called when a remote notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
NSDictionary *aps = [remoteNotification valueForKey:#"aps"];
if (!IsEmpty(aps)) {
NSDictionary *alert = [aps valueForKey:#"alert"];
if (!IsEmpty(alert)) {
NSString *reminderId = [remoteNotification valueForKey:kAppLocalNotificationIdKey]; // This is a custom key
[self manageNotification:alert withId:reminderId];
}
}
// After populating your dynamic notification interface call the completion block.
completionHandler(WKUserNotificationInterfaceTypeCustom);
}
- (void) manageNotification:(NSDictionary *)notificationData withId:(NSString *)notificationId {
NSString *title = [notificationData valueForKey:#"title"];
NSString *message = [notificationData valueForKey:#"body"];
[self.titleLabel setText:title];
[self.messageLabel setText:message];
}
Related
I used the Solution form iOS Share Extension issue when sharing images from Photo library to get Images from the Photo App. This works great in the Simulator, but on the Device I get an error that I can't Access the NSURL provided by the itemProvider:
2018-02-18 12:54:09.448148+0100 MyApp[6281:1653186] [default] [ERROR] Failed to determine whether URL /var/mobile/Media/PhotoData/OutgoingTemp/554581B2-950C-4CFD-AE67-A2342EDEA04D/IMG_2784.JPG (s) is managed by a file provider
Caused by the Statment:
[itemProvider loadItemForTypeIdentifier:itemProvider.registeredTypeIdentifiers.firstObject options:nil completionHandler:^(id<NSSecureCoding> item, NSError *error) {
}
Searching PHAsset for the Item Name is not a good solution as the user have to grand access to the photo library again.
In didSelectPost you must not dismiss the ShareExtension ViewController until you have processed all the items in the inputItems array. The ViewController is dismissed using [super didSelectPost] or by calling the completion method of the extension context.
Here is my solution in code:
- (void)didSelectPost {
__block NSInteger itemCount = ((NSExtensionItem*)self.extensionContext.inputItems[0]).attachments.count;
__block NSInteger processedCount = 0;
for (NSItemProvider* itemProvider in ((NSExtensionItem*)self.extensionContext.inputItems[0]).attachments ) {
if([itemProvider hasItemConformingToTypeIdentifier:#"public.jpeg"]) {
NSLog(#"itemprovider = %#", itemProvider);
[itemProvider loadItemForTypeIdentifier:#"public.jpeg" options:nil completionHandler: ^(id<NSSecureCoding> item, NSError *error) {
NSData *imgData;
if([(NSObject*)item isKindOfClass:[NSURL class]]) {
imgData = [NSData dataWithContentsOfURL:(NSURL*)item];
}
if([(NSObject*)item isKindOfClass:[UIImage class]]) {
imgData = UIImagePNGRepresentation((UIImage*)item);
}
NSDictionary *dict = #{
#"imgData" : imgData,
#"name" : self.contentText
};
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:#"group.share.extension1"];
[defaults setObject:dict forKey:#"img"];
[defaults synchronize];
processedCount += 1;
if (processedCount == itemCount)
[super didSelectPost];
}];
}
}
The loadItemForTypeIdentifier or in Swift loadItem method is asynchronous so dismissing the UI must be called as the last thing inside its completionHandler.
For example I have:
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
if let item = self.extensionContext?.inputItems[0] as? NSExtensionItem, let attachments = item.attachments {
for provider in attachments {
if provider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
provider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil, completionHandler: {item, error in
// do all you need to do here, i.e.
if let tmpURL = item as? URL {
// etc. etc.
}
// and, at the end, inside the completionHandler, call
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
})
}
}
}
}
I you dismiss the UI via:
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
or via:
super.didSelectPost()
outside of the completionHandler after the async method loadItem you will get all kind of permission errors, further more this errors could be random, sometimes happen and sometimes don't, this is because sometimes your async call to loadItem gets the chance to terminate before the UI is dismissed and sometimes it doesn't.
Just leaving this here, hoping it helps someone. This issue costed me few hours.
I have got two buttons on my today extension. One is dedicated to open MainViewController and the second one need to navigate to second ViewController.
So I made a appURL:
let appURL = NSURL(string: "StarterApplication://")
and then on a first button I call:
self.extensionContext?.open(appURL! as URL, completionHandler:nil)
Which opens app on MainViewController.
How can I open MainViewController and performSegue to my SecondViewController when tapping second button on Today Widget?
I made a second URL scheme for that specific ViewController. I saw in other simmilar topics that it can be done by calling:
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
}
in AppDelegate but don't have any idea how to use it.
Use one URL scheme. You can add different paths or arguments for different task.
For example, I've an app that displays multiple items in a today extension. If you tap an item the app is opened.
- (void)_openItem:(SPItem *)item
{
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:#"myapp://localhost/open_item?itemID=%#", item.itemID]];
if (url != nil)
{
[self.extensionContext openURL:url completionHandler:nil];
}
}
As you've already mentioned in you question, you need to implement - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *,id> *)options
In my case it more or less looks like this:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *,id> *)options
{
BOOL ret;
if ([url.scheme isEqualToString:#"myapp"])
{
if ([url.path isEqual:#"open_item"])
{
#try
{
NSDictionary *query = [url.query queryDictionary];
NSString* itemID = query[#"itemID"];
[self _gotoItemWithID:itemID completionHandler:nil];
}
#catch (NSException *exception) {
}
ret = YES;
}
}
else
{
ret = NO;
}
return ret;
}
As you can see, I first check, if the url scheme. If it's the one I expect, I check the path. By using different paths I'm able to implement different commands the today extension is able to execute. Each command may have different arguments. In case of the "open_item" command, I expect the parameter "itemID".
By returning YES, you tell iOS you were able to handle the URL your app was called with.
In my app [self _gotoItemWithID:itemID completionHandler:nil] does all the need tasks to display the item. In your case you would need a function to display the second view controller.
Edit:
I forgot to mention that queryDictionary is a method in an NSString extension. It takes a string (self) and tries to extract URL parameter and return them as dictionary.
- (NSDictionary*)queryDictionary
{
NSCharacterSet* delimiterSet = [NSCharacterSet characterSetWithCharactersInString:#"&;"];
NSMutableDictionary* pairs = [NSMutableDictionary dictionary];
NSScanner* scanner = [[NSScanner alloc] initWithString:self];
while (![scanner isAtEnd])
{
NSString* pairString;
[scanner scanUpToCharactersFromSet:delimiterSet
intoString:&pairString] ;
[scanner scanCharactersFromSet:delimiterSet intoString:NULL];
NSArray* kvPair = [pairString componentsSeparatedByString:#"="];
if ([kvPair count] == 2)
{
NSString* key = [[kvPair objectAtIndex:0] stringByRemovingPercentEncoding];
NSString* value = [[kvPair objectAtIndex:1] stringByRemovingPercentEncoding];
[pairs setObject:value forKey:key] ;
}
}
return [NSDictionary dictionaryWithDictionary:pairs];
}
I found the solution. The answer is deep linking. Here is my method in appDelegate:
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
let urlPath : String = url.path as String!
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
if(urlPath == "/Action"){
let ActionView: ActionViewController = mainStoryboard.instantiateViewController(withIdentifier: "ActionViewController") as! ActionViewController
self.window?.rootViewController = ActionView
} else if(urlPath == "/VoiceCommandView") {
let VoiceCommandView: ViewController = mainStoryboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
self.window?.rootViewController = VoiceCommandView
}
self.window?.makeKeyAndVisible()
return true
And in the TodayExtensionViewController I defined two URL schemes with same host but different URL paths. And made a simple:
self.extensionContext?.open(startAppURL! as URL, completionHandler:nil)
for first button but changed the urlPath for the second button.
If we go from Swift background to foreground, what is the proper way to [nsObject copy] in Swift?
For example in Objective-C, we would loop through a long array of ALAssets (say like 10,000+) in the background by doing:
[alGroup enumerateAssetsUsingBlock:^(ALAsset *alPhoto, NSUInteger index, BOOL *stop)
{
// Here to make changes for speed up image loading from device library...
// =====================================================
// >>>>>>>>>>>>>>>>>>> IN BACKGROUND <<<<<<<<<<<<<<<<<<<
// =====================================================
if(alPhoto == nil)
{
c(#"number of assets to display: %d", (int)bgAssetMedia.count);
// c(#"All device library photos uploaded into memory...%#", bgAssetMedia);
dispatch_async(dispatch_get_main_queue(), ^(void)
{
// =====================================================
// >>>>>>>>>>>>>>>>>>> IN FOREGROUND <<<<<<<<<<<<<<<<<<<
// =====================================================
[ui hideSpinner];
if (_bReverse)
// Here we copying all the photos from device library into array (_assetPhotos)...
_assetPhotos = [[NSMutableArray alloc] initWithArray:[[[bgAssetMedia copy] reverseObjectEnumerator] allObjects]];
else
_assetPhotos = [[NSMutableArray alloc] initWithArray:[bgAssetMedia copy]];
// NSLog(#"%lu",(unsigned long)_assetPhotos.count);
if (_assetPhotos.count > 0)
{
result(_assetPhotos);
}
});
} else {
// if we have a Custom album, lets remove all shared videos from the Camera Roll
if (![self isPhotoInCustomAlbum:alPhoto])
{
// for some reason, shared glancy videos still show with 00:00 minutes and seconds, so remove them now
BOOL isVideo = [[alPhoto valueForProperty:ALAssetPropertyType] isEqual:ALAssetTypeVideo];
int duration = 0;
int minutes = 0;
int seconds = 0;
// NSString *bgVideoLabel = nil;
if (isVideo)
{
NSString *strduration = [alPhoto valueForProperty:ALAssetPropertyDuration];
duration = [strduration intValue];
minutes = duration / 60;
seconds = duration % 60;
// bgVideoLabel = [NSString stringWithFormat:#"%d:%02d", minutes, seconds];
if (minutes > 0 || seconds > 0)
{
[bgAssetMedia addObject:alPhoto];
}
} else {
[bgAssetMedia addObject:alPhoto];
}
}
}
// NSLog(#"%lu",(unsigned long)bgAssetMedia.count);
}];
Then, we would switch to the foreground to update the UIViewController, which are these lines in the above snippet:
_assetPhotos = [[NSMutableArray alloc] initWithArray:[bgAssetMedia copy]];
The "copy" function was the black magic that allowed us to quickly marshal the memory from background to foreground without having to loop through array again.
Is there a similar method in Swift? Perhaps something like this:
_assetPhotos = NSMutableArray(array: bgAssetMedia.copy())
Is Swift thread safe now for passing memory pointers from background to foreground? What's the new protocol? Thank you-
I found the answer. After running large queries on the Realm and CoreData database contexts. I found it easy to just make a basic copy of the memory pointer and downcast it to match the class.
let mediaIdFG = mediaId.copy() as! String
Full example in context below:
static func createOrUpdate(dictionary:NSDictionary) -> Promise<Media> {
// Query and update from any thread
return Promise { fulfill, reject in
executeInBackground {
// c("BG media \(dictionary)")
let realm:RLMRealm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
let media = Media.createOrUpdateInRealm(realm, withJSONDictionary:dictionary as [NSObject : AnyObject])
// media.type = type
c("BG media \(media)")
let mediaId = media.localIdentifier
do {
try realm.commitWriteTransaction()
executeInForeground({
let mediaIdFG = mediaId.copy() as! String
let newMedia = Media.findOneByLocalIdentifier(mediaIdFG)
c("FG \(mediaIdFG) newMedia \(newMedia)")
fulfill(newMedia)
})
} catch {
reject( Constants.createError("Realm Something went wrong!") )
}
}
} // return promise
} // func createOrUpdate
Posting my own answer to let you know my findings. I also found this helpful article about Swift's copy() aka objc's copyWithZone: https://www.hackingwithswift.com/example-code/system/how-to-copy-objects-in-swift-using-copy
I'm trying to use BatteryCenter and CommonUtilities private frameworks under iOS 9.1 with the help of nst's iOS Runtime Headers. It's for research purposes and won't make it to the AppStore.
Here are their respective codes:
- (void)batteryCenter {
NSBundle *bundle = [NSBundle bundleWithPath:#"/System/Library/PrivateFrameworks/BatteryCenter.framework"];
BOOL success = [bundle load];
if(success) {
Class BCBatteryDevice = NSClassFromString(#"BCBatteryDevice");
id si = [[BCBatteryDevice alloc] init];
NSLog(#"Charging: %#", [si valueForKey:#"charging"]);
}
}
- (void)commonUtilities {
NSBundle *bundle = [NSBundle bundleWithPath:#"/System/Library/PrivateFrameworks/CommonUtilities.framework"];
BOOL success = [bundle load];
if(success) {
Class CommonUtilities = NSClassFromString(#"CUTWiFiManager");
id si = [CommonUtilities valueForKey:#"sharedInstance"];
NSLog(#"Is Wi-Fi Enabled: %#", [si valueForKey:#"isWiFiEnabled"]);
NSLog(#"Wi-Fi Scaled RSSI: %#", [si valueForKey:#"wiFiScaledRSSI"]);
NSLog(#"Wi-Fi Scaled RSSI: %#", [si valueForKey:#"lastWiFiPowerInfo"]);
}
}
Although I get the classes back, all of their respected values are NULL which is weird since some must be true, e.g. I'm connected to Wi-Fi so isWiFiEnabled should be YES.
What exactly is missing that my code doesn't return whats expected? Does it need entitlement(s)? If so what exactly?
In Swift, I managed to get this working without the BatteryCenter headers. I'm still looking for a way to access the list of attached batteries without using BCBatteryDeviceController, but this is what I have working so far:
Swift 3:
guard case let batteryCenterHandle = dlopen("/System/Library/PrivateFrameworks/BatteryCenter.framework/BatteryCenter", RTLD_LAZY), batteryCenterHandle != nil else {
fatalError("BatteryCenter not found")
}
guard let batteryDeviceControllerClass = NSClassFromString("BCBatteryDeviceController") as? NSObjectProtocol else {
fatalError("BCBatteryDeviceController not found")
}
let instance = batteryDeviceControllerClass.perform(Selector(("sharedInstance"))).takeUnretainedValue()
if let devices = instance.value(forKey: "connectedDevices") as? [AnyObject] {
// You will have more than one battery in connectedDevices if your device is using a Smart Case
for battery in devices {
print(battery)
}
}
Swift 2.2:
guard case let batteryCenterHandle = dlopen("/System/Library/PrivateFrameworks/BatteryCenter.framework/BatteryCenter", RTLD_LAZY) where batteryCenterHandle != nil else {
fatalError("BatteryCenter not found")
}
guard let c = NSClassFromString("BCBatteryDeviceController") as? NSObjectProtocol else {
fatalError("BCBatteryDeviceController not found")
}
let instance = c.performSelector("sharedInstance").takeUnretainedValue()
if let devices = instance.valueForKey("connectedDevices") as? [AnyObject] {
// You will have more than one battery in connectedDevices if your device is using a Smart Case
for battery in devices {
print(battery)
}
}
This logs:
<BCBatteryDevice: 0x15764a3d0; vendor = Apple; productIdentifier = 0; parts = (null); matchIdentifier = (null); baseIdentifier = InternalBattery-0; name = iPhone; percentCharge = 63; lowBattery = NO; connected = YES; charging = YES; internal = YES; powerSource = YES; poweredSoureState = AC Power; transportType = 1 >
You need to first access the BCBatteryDeviceController, after success block is executed, through which you can get list of all the connected devices.
Here is the code for the same.
Class CommonUtilities = NSClassFromString(#"BCBatteryDeviceController");
id si = [CommonUtilities valueForKey:#"sharedInstance"];
BCBatteryDeviceController* objBCBatteryDeviceController = si;
NSLog(#"Connected devices: %#", objBCBatteryDeviceController.connectedDevices);
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:
So the question now becomes: is there any way to "force" an MKDirectionsRequest to happen in the background?
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()
// ...
}
Your completion handler has an error object you should check what's passed in.
openParentApplication and handleWatchKitExtensionRequest worked fine in Xcode 6.2 Beta 2 and seem to be broken as of Xcode 6.2 Beta 3 (6C101). I always get the error
Error Domain=com.apple.watchkit.errors Code=2
"The UIApplicationDelegate in the iPhone App never called reply() in -[UIApplicationDelegate ...]"
So we probably have to wait for the next beta.
In the code extract that you gave us (which I presume was from handleWatchKitExtensionRequest, although you don't specifically indicate so), you do not call the reply block that was passed to your iPhone app in openParentApplication. That is the first thing that should be checked in these scenarios for other developers with this issue.
However, your second update indicates that it is working fine when the iPhone app is in the foreground. This almost certainly indicates that the issue is one of location services permissions. If your app has permission to access location services when it is running, but does not have "always" permissions, then your WatchKit Extension will not be able to receive results from MapKit when your iPhone app is not running. Requesting (and receiving) such a permission should resolve your issue.
For people more generally who have the problem of not seeing the reply block being called, in Swift that method is defined,
optional func application(_ application: UIApplication!,
handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!,
reply reply: (([NSObject : AnyObject]!) -> Void)!)
Reply thus provides you with a block, to which you execute while passing it AnyObject. You must return something, even if it is reply(nil), or you will get the error message, "The UIApplicationDelegate in the iPhone App never called reply()..."
In Objective-C, the method is defined,
- (void)application:(UIApplication *)application
handleWatchKitExtensionRequest:(NSDictionary *)userInfo
reply:(void (^)(NSDictionary *replyInfo))reply
Note that here, replyInfo must be an NSDictionary that is serializable to a property list file. The contents of this dictionary are still at your discretion and you may specify nil.
Interestingly therefore, this might be a good example of an API where there is a clear advantage in using Swift over Objective-C, as in Swift you can apparently simply pass any object, without the need to serialise many objects to NSData chunks in order to be able to pass them via the NSDictionary in Objective-C.
I had a similar issue, in my case, it turns out that the dictionary being returned needs to have data that can be serialized. If you are trying to return CLLocation data, you will need to use NSKeyedArchiver/NSKeyedUnarchiver to serialize it or convert it into an NSString before passing it to the reply().
Thanks Romain, your code saved my day. I just converted to 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 ]);
}
})
})
})
Here is how we implemented 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;
}