I have a Watch OS 2 application that communicates with the iOS app via WCSession method sendMessage:replyHandler:errorHandler:
The iOS application reply correctly but time to time I get the error with code 7014 of domain WCErrorDomain: "Payload could not be delivered"
It happens more often when the iOS application is not foreground.
I do not find any solution of this problem, I hope one of you know a solution to this problem
In my case, I had to implement both delegates:
The one without any replyHandler
func session(_ session: WCSession,
didReceiveMessage message: [String : Any])
The one with replyHandler
func session(_ session: WCSession,
didReceiveMessage message: [String : Any],
replyHandler: #escaping ([String : Any]) -> Void)
If you send a message without a replyHandler then the first delegate runs.
If you send a message with a replyHandler then the second delegate runs.
In some cases I was sending only a message, and in other cases I was sending a message and expecting a reply from the counterpart.
BUT... I had implemented only the second delegate -_-
Anyways, eventually to reduce duplicate code I implemented a common method and ended up with:
func session(_ session: WCSession,
didReceiveMessage message: [String : Any]) {
handleSession(session,
didReceiveMessage: message)
}
func session(_ session: WCSession,
didReceiveMessage message: [String : Any],
replyHandler: #escaping ([String : Any]) -> Void) {
handleSession(session,
didReceiveMessage: message,
replyHandler: replyHandler)
}
//Helper Method
func handleSession(_ session: WCSession,
didReceiveMessage message: [String : Any],
replyHandler: (([String : Any]) -> Void)? = nil) {
//Common logic
}
Watch OS 4
For anyone having issues on iOS10 beta 6 and GM, and you are using Swift3, the solution is to change the delegate function header in the iOS app to the following:
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
Note the #escaping and the Any instead of the AnyObject type.
Try this one, this fixed my issue. Inside the InterfaceController add the following methods for passing the data to phone.
-(void)sendDataToPhone:(NSDictionary* _Nonnull)dictData
{
if(WCSession.isSupported){
WCSession* session = WCSession.defaultSession;
session.delegate = self;
[session activateSession];
if(session.reachable)
{
[session sendMessage:dictData replyHandler: ^(NSDictionary<NSString *,id> * __nonnull replyMessage) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#".....replyHandler called --- %#",replyMessage);
// Play a sound in watch
[[WKInterfaceDevice currentDevice] playHaptic:WKHapticTypeSuccess];
});
}
errorHandler:^(NSError * __nonnull error) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Error = %#",error.localizedDescription);
});
}
];
}
else
NSLog(#"Session Not reachable");
}
else
NSLog(#"Session Not Supported");
}
#pragma mark - Standard WatchKit delegate
-(void)sessionWatchStateDidChange:(nonnull WCSession *)session
{
if(WCSession.isSupported){
WCSession* session = WCSession.defaultSession;
session.delegate = self;
[session activateSession];
}
}
In the phone side, add the following codes for receiving the data from watch.
Add the following in didFinishLaunchingWithOptions.
// Allocating WCSession inorder to communicate back to watch.
if(WCSession.isSupported){
WCSession* session = WCSession.defaultSession;
session.delegate = self;
[session activateSession];
}
Now add the WCSessionDelegate.
#pragma mark - WCSession Delegate
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler
{
if(message){
NSData *receivedData = [message objectForKey:#"AudioData"];
NSDictionary* response = #{#"response" : [NSString stringWithFormat:#"Data length: %lu",(unsigned long)receivedData.length]} ;
replyHandler(response);
}
}
#pragma mark - Standard WatchKit Delegate
-(void)sessionWatchStateDidChange:(nonnull WCSession *)session
{
if(WCSession.isSupported){
WCSession* session = WCSession.defaultSession;
session.delegate = self;
[session activateSession];
if(session.reachable){
NSLog(#"session.reachable");
}
if(session.paired){
if(session.isWatchAppInstalled){
if(session.watchDirectoryURL != nil){
}
}
}
}
}
Hope this helps you :)
Sorry I'dont have enough reputation to comment answers.
My issue got resolved with Peter Robert's answer:
With Swift 3 WCErrorCodeDeliveryFailed appeared and the solution was simply changing AnyObject to Any on the replyHandlers.
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
//code
replyHandler (answer as [String : Any])
}
I was experiencing the same and moving WCSession initialization (setting delegate and activating it) later in the app lifecycle fixed the issue.
I had WCSession activation in app delegates didFinishLaunching and having it there broke the communication. Moving WCSession intialization later in the app made comms work again.
In my case I put WCSessionDelegate(iOS side) in a separate class and initialize it as local variable. Changing it to global instance variable solved the issue.
So my iOS Code was:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
SessionHandler()
}
Changed to below to get it working:
var handler: SessionHandler!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
handler = SessionHandler()
}
You may need to (check and) implement that your WCSession delegate implemented the following method. I got this error due to missing the implementation.
- (void)session:(WCSession * _Nonnull)session
didReceiveMessage:(NSDictionary<NSString *, id> * _Nonnull)replyMessage
replyHandler:(void (^ _Nonnull)(NSDictionary<NSString *, id> * _Nonnull replyMessage))replyHandler
{
NSLog(#"Received. %#", replyMessage);
[self processResponse:replyMessage];
}
Check the delegate connected correct ?
WCSession* session = WCSession.defaultSession;
session.delegate = self;
[session activateSession];
Note : Verify session.delegate = self; set to self.
Working on an app and have the exact same behavior.
I feel fairly sure that I have looked everywhere in my code and found nothing wrong. My best guess is that this must be a bug with WatchConnectivity.
My current error handler workaround simply tries to reload data on this particular error. Not very beautiful, but it works ok.
You might want to try something similar?
func messageErrorHandler(error: NSError) {
isLoading = false
print("Error Code: \(error.code)\n\(error.localizedDescription)")
// TODO: WTF?. Check future releases for fix on error 7014, and remove this...
if error.code == 7014 {
// Retry after 1.5 seconds...
retryTimer = NSTimer.scheduledTimerWithTimeInterval(
NSTimeInterval(1.5), target: self, selector: "reloadData", userInfo: nil, repeats: false)
return
}
displayError("\(error.localizedDescription) (\(error.code))",
message: "\(error.localizedDescription)")
}
UPDATE:
For anyone working with WatchConnectivity; I need to have a similar "hack" for testing the session.reachable variable.
I have noticed that my app manages to send a message before the session becomes reachable. So I simply try to reload data (re-send the message) a couple of times before actually telling the user their phone is out of reach.
UPDATE 2:
The above example is using .sessionWatchStateDidChange(), so the issue is not that .sendMessage() is triggered too early because of not waiting for connection ack. This must be a bug as it is not happening every time, it just freaks out like 1 per 100 messages.
I have found that putting the reply code as the first thing to run fixes this issue (possible being caused by timing out?).
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: ([String : Any]) -> Void) {
print("Message - received")
//Send reply
let data = ["receivedData" : true]
replyHandler(data as [String : AnyObject])
}
In Swift 3 I solved implementing didReceiveMessage with this signature:
func session(_ session: WCSession, didReceiveMessage message: [String : Any],
replyHandler: #escaping ([String : Any]) -> Void)
Be sure that your Session is always active. I for example had an other View which was part of the testing and then returned on the initially View and was wondering why the Session wasn't active anymore.
- (void)willActivate {
// This method is called when watch view controller is about to be visible to user
[super willActivate];
//Setup WCSession
if ([WCSession isSupported]) {
[[WCSession defaultSession] setDelegate:self];
[[WCSession defaultSession] activateSession];
}}
Above did it for me. Had it first placed in the awakeWithContext, stupid me....
This scenario will cover several use case. Please take look at these steps , it help me a lot.
1 - Understand that each device must have their own WCSession instance configured and with the appropriate delegates configured.
2 - implement WCSessionDelegate only at one single place on each device, ej. on iOS app on the AppDelegate, on watchOS on ExtensionDelegate. This is very important because with the appropriate WCSession configured on watchOS but on iPhone implementing on two different place, ej. on app delegate and then on the first viewcontorllweer of the app, (on my case ) lead to unstable behaviour and thats the main reason why sometimes the iOS app stop responding when message received from watch.
3 - reactivating the session is advisable only do it on the Host App. This is an example of my iOS App with only one WCSessionDelegate.
(AppDelegate )
#pragma mark - WCSessionDelegate
- (void)session:(WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(NSError *)error{
if( activationState == WCSessionActivationStateActivated) {
NSLog(#"iPhone WKit session Activated");
}else if (activationState == WCSessionActivationStateInactive) {
NSLog(#"iPhone WKit Inactive");
}else if (activationState == WCSessionActivationStateNotActivated) {
NSLog(#"iPhone WKit NotActivated");
}
}
- (void)sessionDidBecomeInactive:(WCSession *)session{
/*
The session calls this method when it detects that the user has switched to a different Apple Watch. While in the inactive state, the session delivers any pending data to your delegate object and prevents you from initiating any new data transfers. After the last transfer finishes, the session moves to the deactivated state
*/
NSLog(#"sessionDidBecomeInactive");
if (session.hasContentPending) {
NSLog(#"inactive w/ pending content");
}
}
- (void)sessionDidDeactivate:(WCSession *)session{
// Begin the activation process for the new Apple Watch.
[[WCSession defaultSession] activateSession];
//perform any final cleanup tasks related to closing out the previous session.
}
- (void)sessionReachabilityDidChange:(WCSession *)session{
NSLog(#"sessionReachabilityDidChange");
}
last thing, write the appropriate method signature, if you need a reply sending data from watch , take the method signature who have reply:...
According to apple the following methods
sendMessage:replyHandler:errorHandler:, sendMessageData:replyHandler:errorHandler:, and transferCurrentComplicationUserInfo:
has a higher priority and is transmitted right away. All messages received by your app are delivered to the session delegate serially on a background thread.
So do not waste time dispatching the reply object on mainQueue on the iOS appDelegate, wait till you have the response on your watchOS back and change it to main thread to update your UI accordingly.
Related
I have a custom function for capturing true depth camera information and the function gets returned before the delegate functions have finished processing the captured photo. I need to somehow wait until the delegates have all completed before I return the correct value.
I tried wrapping the main function call into a synchronized block, but that did not solve the problem.
- (NSDictionary *)capture:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
{
if (#available(iOS 11.1, *)) {
// Set photosettings to capture depth data
AVCapturePhotoSettings *photoSettings = [AVCapturePhotoSettings photoSettingsWithFormat:#{AVVideoCodecKey : AVVideoCodecJPEG}];
photoSettings.depthDataDeliveryEnabled = true;
photoSettings.depthDataFiltered = false;
#synchronized(self) {
[self.photoOutput capturePhotoWithSettings:photoSettings delegate:self];
}
}
// Somehow need to wait here until the delegate functions finish before returning
return self.res;
}
The delegate function which gets called too late:
- (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error
{
Cam *camera = [[Cam alloc] init];
self.res = [camera extractDepthInfo:photo];
}
Currently nil is returned before the delegate gets ever called and only afterwards does the delegate function assign the desired result to self.res
I believe that what you looking for is dispatch_semaphore_t.
Semaphores allow you to lock a thread until a secondary action is performed. This way, you can postpone the return of the method until the delegate has returned (if you are operating on a secondary thread).
The problem with such an approach is that you will be locking the thread! So, if you are operating in the main thread, your app will become unresponsive.
I would recommend you to consider moving the response to a completion block, similar to:
-(void)capture:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject completion:(void (^)(NSDicitionary* ))completion {
self.completion = completion
...
}
And call the completion at the end:
- (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error
{
Cam *camera = [[Cam alloc] init];
self.res = [camera extractDepthInfo:photo];
self.completion(self.res);
}
=== Edit: Swift Code ===
The code above would be translated to something like:
var completion: (([AnyHashable: Any]) -> Void)?
func capture(options: [AnyHashable: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock, completion: #escaping ([AnyHashable: Any]) -> Void) {
self.completion = completion
...
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
let cam = Cam()
let result = cam.extractDepthInfo(photo)
self.completion?(result)
}
An important note here is that the completion needs to be marked as #escaping in the capture method, given that the object will be copied.
I found many questions and many answers but no final example for the request:
Can anyone give a final example in Objective C what is best practice to use WCSession with an IOS app and a Watch app (WatchOS2) with more than one ViewController.
What I noticed so far are the following facts:
1.) Activate the WCSession in the parent (IOS) app at the AppDelegate:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//Any other code you might have
if ([WCSession isSupported]) {
self.session = [WCSession defaultSession];
self.session.delegate = self;
[self.session activateSession];
}
}
2.) On the WatchOS2 side use <WCSessionDelegate>. But the rest is totally unclear for me! Some answers are talking from specifying keys in the passing Dictionary like:
[session updateApplicationContext:#{#"viewController1": #"item1"} error:&error];
[session updateApplicationContext:#{#"viewController2": #"item2"} error:&error];
Others are talking about retrieving the default session
WCSession* session = [WCSession defaultSession];
[session updateApplicationContext:applicationDict error:nil];
Others are talking about different queues? "It is the client's responsibility to dispatch to another queue if necessary. Dispatch back to the main."
I am totally confused. So please give an example how to use WCSession with an IOS app and a WatchOS2 App with more than one ViewController.
I need it for the following case (simplified):
In my parent app I am measuring heart rate, workout time and calories. At the Watch app 1. ViewController I will show the heart rate and the workout time at the 2. ViewController I will show the heart rate, too and the calories burned.
As far as I understand the task you just need synchronisation in a Phone -> Watch direction so in a nutshell a minimum configuration for you:
Phone:
I believe the application:didFinishLaunchingWithOptions: handler is the best place for the WCSession initialisation therefore place the following code there:
if ([WCSession isSupported]) {
// You even don't need to set a delegate because you don't need to receive messages from Watch.
// Everything that you need is just activate a session.
[[WCSession defaultSession] activateSession];
}
Then somewhere in your code that measures a heart rate for example:
NSError *updateContextError;
BOOL isContextUpdated = [[WCSession defaultSession] updateApplicationContext:#{#"heartRate": #"90"} error:&updateContextError]
if (!isContextUpdated) {
NSLog(#"Update failed with error: %#", updateContextError);
}
update:
Watch:
ExtensionDelegate.h:
#import WatchConnectivity;
#import <WatchKit/WatchKit.h>
#interface ExtensionDelegate : NSObject <WKExtensionDelegate, WCSessionDelegate>
#end
ExtensionDelegate.m:
#import "ExtensionDelegate.h"
#implementation ExtensionDelegate
- (void)applicationDidFinishLaunching {
// Session objects are always available on Apple Watch thus there is no use in calling +WCSession.isSupported method.
[WCSession defaultSession].delegate = self;
[[WCSession defaultSession] activateSession];
}
- (void)session:(nonnull WCSession *)session didReceiveApplicationContext:(nonnull NSDictionary<NSString *,id> *)applicationContext {
NSString *heartRate = [applicationContext objectForKey:#"heartRate"];
// Compose a userInfo to pass it using postNotificationName method.
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:heartRate forKey:#"heartRate"];
// Broadcast data outside.
[[NSNotificationCenter defaultCenter] postNotificationName: #"heartRateDidUpdate" object:nil userInfo:userInfo];
}
#end
Somewhere in your Controller, let's name it XYZController1.
XYZController1:
#import "XYZController1.h"
#implementation XYZController1
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleUpdatedHeartRate:) name:#"heartRateDidUpdate" object:nil];
}
-(void)handleUpdatedHeartRate:(NSNotification *)notification {
NSDictionary* userInfo = notification.userInfo;
NSString* heartRate = userInfo[#"heartRate"];
NSLog (#"Successfully received heartRate notification!");
}
#end
Code hasn't been tested I just wrote it as is so there can be some typos.
I think the main idea now is quite clear and a transfer of remaining types of data is not that tough task.
My current WatchConnectivity architecture much more complicated but nevertheless it is based on this logic.
If you still have any questions we might move a further discussion to the chat.
Well, this is simplified version of my solution as requested by Greg Robertson. Sorry it's not in Objective-C anymore; I'm just copy-pasting from existing AppStore-approved project to make sure there will be no mistakes.
Essentially, any WatchDataProviderDelegate can hook to data provider class as that provides array holder for delegates (instead of one weak var).
Incoming WCSessionData are forwarded to all delegates using the notifyDelegates() method.
// MARK: - Data Provider Class
class WatchDataProvider: WCSessionDelegate {
// This class is singleton
static let sharedInstance = WatchDataProvider()
// Sub-Delegates we'll forward to
var delegates = [AnyObject]()
init() {
if WCSession.isSupported() {
WCSession.defaultSession().delegate = self
WCSession.defaultSession().activateSession()
WatchDataProvider.activated = true;
}
}
// MARK: - WCSessionDelegate
public func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
processIncomingMessage(userInfo)
}
public func session(session: WCSession, didReceiveApplicationContext applicationContext: [String: AnyObject]) {
processIncomingMessage(applicationContext)
}
func processIncomingMessage(dictionary: [String:AnyObject] ) {
// do something with incoming data<
notifyDelegates()
}
// MARK: - QLWatchDataProviderDelegate
public func addDelegate(delegate: AnyObject) {
if !(delegates as NSArray).containsObject(delegate) {
delegates.append(delegate)
}
}
public func removeDelegate(delegate: AnyObject) {
if (delegates as NSArray).containsObject(delegate) {
delegates.removeAtIndex((delegates as NSArray).indexOfObject(delegate))
}
}
func notifyDelegates()
{
for delegate in delegates {
if delegate.respondsToSelector("watchDataDidUpdate") {
let validDelegate = delegate as! WatchDataProviderDelegate
validDelegate.watchDataDidUpdate()
}
}
}
}
// MARK: - Watch Glance (or any view controller) listening for changes
class GlanceController: WKInterfaceController, WatchDataProviderDelegate {
// A var in Swift is strong by default
var dataProvider = WatchDataProvider.sharedInstance()
// Obj-C would be: #property (nonatomic, string) WatchDataProvider *dataProvider
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
dataProvider.addDelegate(self)
}
// WatchDataProviderDelegate
func watchDataDidUpdate() {
dispatch_async(dispatch_get_main_queue(), {
// update UI on main thread
})
}}
}
class AnyOtherClass: UIViewController, WatchDataProviderDelegate {
func viewDidLoad() {
WatchDataProvider.sharedInstance().addDelegate(self)
}
// WatchDataProviderDelegate
func watchDataDidUpdate() {
dispatch_async(dispatch_get_main_queue(), {
// update UI on main thread
})
}}
}
Doing the session management (however WCSession is singleton) in a View-Controller smells like MVC violation (and I've seen too many Watch blog posts wrong this way already).
I made an umbrella singleton class over the WCSession, that is first strongly referenced from Watch Extension Delegate to make sure it will load soon and do not get deallocated in the middle of work (e.g. when a View-Controller disappears while transferUserInfo or transferCurrentComplicationUserInfo happens in another watch thread).
Only this class then handles/holds the WCSession and decouples the session data (Model) from all the View-Controller(s) in watch app, exposing data mostly though public static class variables providing at least basic level of thread-safety.
Then this class is used both from complication controller, glance controller and other view controllers. Updates run in the background (or in backgroundFetchHandler), none of the apps (iOS/WatchOS) is required to be in foreground at all (as in case of updateApplicationContext) and the session does not necessarily have to be currently reachable.
I don't say this is ideal solution, but finally it started working once I did it this way. I'd love to hear that this is completely wrong, but since I had lots of issues before going with this approach, I'll stick to it now.
I do not give code example intentionally, as it is pretty long and I don't want anyone to blindly copy-paste it.
I found, with "try and error", a solution. It is working, but I don't know exactly why! If I send a request from the Watch to the IOS app, the delegate of that ViewController of the Watch app gets all the data from the main queue from the IOS app. I added the following code in the - (void)awakeWithContext:(id)context and the - (void)willActivate of all ViewControllers of the Watch app:
By example 0 ViewController:
[self packageAndSendMessage:#{#"request":#"Yes",#"counter":[NSString stringWithFormat:#"%i",0]}];
By example 1 ViewController1:
[self packageAndSendMessage:#{#"request":#"Yes",#"counter":[NSString stringWithFormat:#"%i",1]}];
/*
Helper function - accept Dictionary of values to send them to its phone - using sendMessage - including replay from phone
*/
-(void)packageAndSendMessage:(NSDictionary*)request
{
if(WCSession.isSupported){
WCSession* session = WCSession.defaultSession;
session.delegate = self;
[session activateSession];
if(session.reachable)
{
[session sendMessage:request
replyHandler:
^(NSDictionary<NSString *,id> * __nonnull replyMessage) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#".....replyHandler called --- %#",replyMessage);
NSDictionary* message = replyMessage;
NSString* response = message[#"response"];
[[WKInterfaceDevice currentDevice] playHaptic:WKHapticTypeSuccess];
if(response)
NSLog(#"WK InterfaceController - (void)packageAndSendMessage = %#", response);
else
NSLog(#"WK InterfaceController - (void)packageAndSendMessage = %#", response);
});
}
errorHandler:^(NSError * __nonnull error) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"WK InterfaceController - (void)packageAndSendMessage = %#", error.localizedDescription);
});
}
];
}
else
{
NSLog(#"WK InterfaceController - (void)packageAndSendMessage = %#", #"Session Not reachable");
}
}
else
{
NSLog(#"WK InterfaceController - (void)packageAndSendMessage = %#", #"Session Not Supported");
}
}
is anywhere a simple tutorial, how I can use the WCSessionUserInfoTransfer to change data between my watch and iOS ?
And what must be written in the delegate of the iOS -App?
in my old programming I used:
[WKInterfaceController openParentApplication:#{#"Kommando":#"Radius"} reply:^(NSDictionary *replyInfo, NSError *error) {
if (error) {
NSLog(#"Error from parent: %#", error);
} else {
NSLog(#"Radius from parent: %#", [replyInfo objectForKey:#"Radius"]);
}
}];
The first thing you need to do is to define a WCSession. This must be defined and activated in every class that you're planning on transferring data to and receiving data from. To use WCSession, make sure it's supported, and then activate the default session as shown below.
#import <WatchConnectivity/WatchConnectivity.h>
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
From here, you can use transferUserInfo where ever you need to send data (from the watch or the iOS app):
[[WCSession defaultSession] transferUserInfo:#{#"Kommando":#"Radius"}];
On the receiving end, you would use session:didReceiveUserInfo. Note that this does NOT need to be in the app delegate anymore on the iOS app side, unlike handleWatchKitExtensionRequest. You can use this where ever you need to receive the data. Make sure to activate WCSession, as shown above, in the class where you have didReceiveUserInfo also.
- (void)session:(nonnull WCSession *)session didReceiveUserInfo:(nonnull NSDictionary<NSString *,id> *)userInfo {
NSLog(#"Radius from parent: %#", [userInfo objectForKey:#"Radius"]);
}
I have currently a problem.
My wanted behaviour: If I open my WatchKit App, I call "openParentApplication". I receive my wanted data.
But if I tested on real devices, it doesnt work since I open the parent app in the iPhone.
But when I'm testing in simulator it works without to open the parent app.
My Xcode Version is 6.3.2 and iOS 8.3.
What could be the problem?
InterfaceController.m
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
NSDictionary *userInfo = #{#"request":#"refreshData"};
[WKInterfaceController openParentApplication:userInfo reply:^(NSDictionary *replyInfo, NSError *error)
{
entries = replyInfo;
NSLog(#"Reply: %#",replyInfo);
[self reloadTable];
[self.city setText:[entries valueForKey:#"city"][0] ];
}];
}
AppDelegate.m
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply
{
NSString *refresh = [userInfo valueForKey:#"request"];
if([refresh isEqualToString:#"refreshData"])
{
NSString *city = [[NSUserDefaults standardUserDefaults] stringForKey:#"City"];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:[NSString stringWithFormat:#"http://blackdriver.adappter.de/api/retrieve.php?city=%#",[city stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject)
{
reply(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"Error: %#", error);
}];
}
}
EDIT - Correct answer:
See the link from mohammed alwaili in the comments
A openParentApplication:reply request must return immediately, so you'll have to request extra time for your asynchronous request to finish (alternately run a synchronous request, but this is terrible practice).
From the Apple WatchKit Developer Tips and Best Practices:
If your app on Apple Watch needs to perform longer running background
tasks, such as networking calls, you should rely on your iPhone app to do the work. Use the openParentApplication:reply: method in WKInterfaceController to wake up your iPhone app in the background and return the data that your WatchKit extension needs. The UIApplicationDelegate method that handles the WatchKit request must return immediately. If an asynchronous call is required, to perform networking for example, use a background task to make sure your app is not suspended before it has a chance to send its reply.
I had a similar issue and had to work through a few problems to have my WatchKit app successfully call iOS app which made an asynchronous API call.
Here's a working code snippet
func application(application: UIApplication, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]?, reply: (([NSObject : AnyObject]!) -> Void)!) {
let backgroundProcessingToken = application.beginBackgroundTaskWithName("backgroundApiCall", expirationHandler: { () -> Void in
reply(["response":["error":"SOME_ERROR_CODE_INDICATING_TIMEOUT"]])
})
request(.GET, "https://api.forecast.io/forecast/[INSERT DARK SKY API CODE]/37.8267,-122.423").responseJSON(options: NSJSONReadingOptions.AllowFragments, completionHandler:{request, response, data, error in
if(error != nil || data == nil){
reply(["response":["error":"SOME_ERROR_CODE_INDICATING_FAILURE"]])
}
if let json = data as? NSDictionary {
reply(["response":["data":json]])
}
application.endBackgroundTask(backgroundProcessingToken)
})
}
Ultimately, you need to register as a background task to ensure your app doesn't get killed by the operating system.
I also have a working example here on github FWIW
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;
}