WCSession: Data submitted on iOS is never received on the Watch - ios

I am working on a small time tracking app for iOS. The user can create any number of activity and then track how much time he spend on each of them (e.g. sleeping, driving to work, eating, working, etc.)
Now I would like to add Apple Watch support: The user should be able to pick an activity from a list on his watch, enter a time value and submit this result to the iOS app which then updates its data.
So, I need communication in both directions:
iOS -> Watch:
List of configured activties
Watch -> iOS
Add time XY for activity 123
I implemented the WCSession on both sides but no matter what I try, there seems to be no communication:
// iOS App Delegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
...
if ([WCSession isSupported]) {
session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
}
- (void)updateActitviesOnWatch:(NSDictionary<NSString *, id> *)data {
if (session != nil && session.paired && session.activationState == WCSessionActivationStateActivated) {
[session updateApplicationContext:data error:nil];
[session sendMessage:data replyHandler:^(NSDictionary<NSString *,id> * _Nonnull replyMessage) {
NSLog(#"sendMessage : success");
} errorHandler:^(NSError * _Nonnull error) {
NSLog(#"sendMessage : error");
}];
[session transferUserInfo:data];
}
}
// WatchExtension ExtensionDelegate.m
- (void)applicationDidFinishLaunching {
if ([WCSession isSupported]) {
session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
}
- (void)session:(WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(nullable NSError *)error {
NSLog(#"WK activationDidCompleteWithState");
}
- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary<NSString *, id> *)userInfo {
NSLog(#"WK session didReceiveUserInfo");
}
- (void)session:(WCSession *)session didReceiveApplicationContext:(NSDictionary<NSString *, id> *)applicationContext {
NSLog(#"WK session didReceiveApplicationContext");
}
- (void)session:(WCSession * __nonnull)session didFinishUserInfoTransfer:(WCSessionUserInfoTransfer *)userInfoTransfer error:(nullable NSError *)error {
NSLog(#"WK session didFinishUserInfoTransfer");
}
No matter which method is used to send the data on iOS (updateApplicationContext:error:, sendMessage:replyHandler:errorHandler or transferUserInfo:) non of the receiving delegate methods is called on the Watch.
What ma I doing wrong?
What is the correct method to transferee the data in the first place?

Related

WCSession activate session not calling delegate method

I have this code written in my Manager class init method.
if([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
Also, implemented these delegate methods in manager class.
- (void)session:(WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(NSError *)error {
if(error) {
NSLog(#"%#", error.description);
}
NSLog(#"iOS App Session activated.");
}
- (void)sessionDidBecomeInactive:(nonnull WCSession *)session {
//
}
- (void)sessionDidDeactivate:(nonnull WCSession *)session {
//
}
- (void)sessionReachabilityDidChange:(WCSession *)session {
//
}
Watch app InterfaceController delegate method:
- (void)session:(WCSession *)session didReceiveApplicationContext:(NSDictionary<NSString *,id> *)applicationContext {
//
}
My problems are:
activationDidCompleteWithState is never being called.
I am calling updateContext method to send data to watch app but didReceiveApplicationContext method never being called in InterfaceController.
You have to retain the WCSession object in a member variable.
#property (nonatomic, strong) WCSession* wcSession;
...
if ([WCSession isSupported])
{
self.wcSession = [WCSession defaultSession];
self.wcSession.delegate = self;
[self.wcSession activateSession];
}
I got the solution for above issue. It is the problem with simulators. So I would prefer to test Watch app with real device. My same code is working fine when tested with real device.

Trouble sending data from iPhone to WatchOS using Application Context

I am trying to send data from my iOS app to a companion WatchOS app using WCSession. The iOS app was created with NativeScript, thus the need for Objective-C.
When running the apps on both simulators and real devices I receive the following error message:
[WC] WCSession is missing its delegate
I've been messing around with this for a few days but unable to fix this issue.
iOS Objective-C Code (This is being called in Typescript):
#import "SendToWatch.h"
#import <WatchConnectivity/WatchConnectivity.h>
#interface SendToWatch () <WCSessionDelegate>
#end
#implementation SendToWatch
- (void)sendData: (double)value {
if (WCSession.isSupported) {
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
NSError *error = nil;
NSDictionary *applicationDict = #{#"data":[NSString stringWithFormat:#"%0.2f", value]};
[session updateApplicationContext:applicationDict error:nil];
if (error) {
NSLog(#"%#", error.localizedDescription);
}
}
}
//MARK: - WCSessionDelegate
- (void)session:(WCSession *)session
activationDidCompleteWithState:(WCSessionActivationState)activationState
error:(NSError *)error {
}
- (void)sessionDidBecomeInactive:(WCSession *)session {
NSLog(#"Session Did Become Inactive");
}
- (void)sessionDidDeactivate:(WCSession *)session {
NSLog(#"-- Session Did Deactivate --");
[session activateSession];
}
#end
WatchOS (InterfaceController.m):
#import "InterfaceController.h"
#import <WatchConnectivity/WatchConnectivity.h>
#interface InterfaceController () <WCSessionDelegate>
#end
#implementation InterfaceController
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
// Creates a WCSession to allow iPhone connectivity
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
NSLog(#"-- WCSession Active --");
}
}
- (void)willActivate {
[super willActivate];
NSLog(#"-- Controller Activated --");
}
- (void)didDeactivate {
[super didDeactivate];
NSLog(#"-- Controller Deactive --");
}
//MARK: - WCSessionDelegate
// Receieves the data sent from the iPhone app
- (void)session:(nonnull WCSession *)session didReceiveApplicationContext:(nonnull NSDictionary *)applicationContext {
NSString *receivedData = [applicationContext objectForKey:#"data"];
NSLog(#"-- APPLICATION CONTEXT RECEIVED --");
NSLog(#"-- Received from iOS App: %#", applicationContext);
dispatch_async(dispatch_get_main_queue(), ^{
[self.dataLabel setText:receivedData];
NSLog(#"-- DATA UPDATED --");
});
}
- (void)session:(WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(NSError *)error {
}
#end
You'll need to move your session configuration and activation code
if ([WCSession isSupported]) {
WCSession* session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
to somewhere early in the iOS app's lifecycle, rather than in the sendData message.
To fix this I had to re-write the Objective-C code in typescript using the tns-platform-declarations plugin. The WCSession now has a delegate and is sending data to my companion WatchOS app.

WCSession send message is not working on the actual device but working on simulator

I'm sending a message from iPhone to Watch (WatchOS2) where there is an internal timer runs on the iPhone. For every 30 seconds, I'm sending the message from the iPhone to watch. Unfortunately, send message is working only for the first time, where my watch is receiving successfully. But, from the next time watch is not receiving any message. The whole scenario works perfectly in the simulator but not on the actual watch. Any help would be appreciated. I tried to send the message from the main thread but of no use. Any idea on where I'm doing wrong.
Thanks in advance.
this is the code I use
At watch side
//InterfaceController1.m
- (void)willActivate {
// This method is called when watch view controller is about to be visible to user
[super willActivate];
if ([WCSession isSupported]) {
self.session = [WCSession defaultSession];
self.session.delegate = self;
[self.session activateSession];
[self.session sendMessage:#{#"OpeniOS":#"YES"} replyHandler:nil errorHandler:nil];
}
}
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler{
//able to receive message - dictionary with IssuerNames Array from iPhone
[self setupTable:message]//I'm setting up my tableview and on click of a row from tableview I'm pushing the user to InterfaceController2.m
}
- (void)table:(WKInterfaceTable *)table didSelectRowAtIndex:(NSInteger)rowIndex{
[self.session sendMessage:#{#"AccSel":#"YES"} replyHandler:nil errorHandler:nil];
[self pushControllerWithName:#"InterfaceController2" context:[NSNumber numberWithInteger:rowIndex]];
}
//InterfaceController2.m
- (void)willActivate {
[super willActivate];
if ([WCSession isSupported]) {
_session = [WCSession defaultSession];
_session.delegate = self;
[_session activateSession];
}
}
-(void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *,id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler{
NSLog(#"%#",message);
}
At iPhone side
//ViewController1.m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
if ([WCSession isSupported]) {
_session=[WCSession defaultSession];
_session.delegate=self;
[_session activateSession];
[_session sendMessage:#{#"testKey":#"testVal"} replyHandler:nil errorHandler:nil];
}
}
-(void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *,id> *)message{
if ([[message objectForKey:#"OpeniOS"] isEqualToString:#"YES"]) {
NSMutableArray *tempIssuerArray=[[NSMutableArray alloc] init];
for (OTPToken *token in self.tokenManager.tokens) {
[tempIssuerArray addObject:token.issuer];
}
if ([_session isReachable]) {
NSDictionary *temp=#{#"IssuerNames":tempIssuerArray};
[_session sendMessage:temp replyHandler:nil errorHandler:nil];
}
}
if ([[message objectForKey:#"AccSel"] isEqualToString:#"YES"]) {
OTPToken *token = [self.tokenManager.tokens objectAtIndex:[[message objectForKey:#"selIndex"] intValue]];
DisplayTokenViewController *dtvc=[self.storyboard instantiateViewControllerWithIdentifier:#"DisplayToken"];
dtvc.token=token;
dtvc.tkm=self.tokenManager;
[self.navigationController pushViewController:dtvc animated:YES];
}
}
//ViewController2.m
-(void)viewDidLoad {
[super viewDidLoad];
mySession=[WCSession defaultSession];
mySession.delegate=self;
[mySession activateSession];
[self refresh]; //this refresh method is called every 30 seconds based on a property change value
}
- (void)refresh{
NSDictionary* dict=#{#"code":#"123",#"name":#"abc"};
[mySession sendMessage:dict replyHandler:nil errorHandler:nil];
}
Actually, on the watch side, InterfaceController1.m is shown to the user at first, on a button click from InterfaceController1.m, user redirects to InterfaceController2.m. At the same time, on iPhone end I'm pushing the ViewController2.m from ViewController1.m on receiving a message from watch.
Here, the refresh method is called for only one time and after every 30 seconds, ideally refresh method should be called but not in the actual device. But everything is working perfectly in the simulator
When you use the WCSession sendMessage API with a nil replyHandler like this:
[_session sendMessage:#{#"testKey":#"testVal"} replyHandler:nil errorHandler:nil];
you need to implement the following delegate method on the receiving side:
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message {
//able to receive message - dictionary with IssuerNames Array from iPhone
[self setupTable:message]//I'm setting up my tableview and on click of a row from tableview I'm pushing the user to InterfaceController2.m
}
rather than session:didReceiveMessage:replyHandler:. So in your example above, it seems that InterfaceController1.m on the watch side should not be able to receive the message like you say it does. I'm guessing that perhaps you just made a copy paste error as you were sanitizing the code for SO.

WCSession on Apple Watch not work properly

I have XCode 7, iPhone6 with iOS 9.1, Apple Watch with WatchOS 2.0 (now I update to 2.0.1)
I try to make communication between Watch and iPhone.
On iPhone I init my singleton
- (instancetype)init {
self = [super init];
if (self.isConnectivityAvailable) {
session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
return self;
}
- (BOOL)isConnectivityAvailable {
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"9.0")) {
return [WCSession isSupported];
} else {
return NO;
}
}
in AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//custom code
(void)[AppConnectivityHandler instance]; //start App connectivity with Apple Watch
return YES;
}
And it is all good
I process message like that
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *,id> *)message replyHandler:(void (^)(NSDictionary<NSString *,id> * _Nonnull))replyHandler {
LOG(#"receive message");
NSString *request = message[kRequestKey];
__block NSDictionary *reply = #{};
dispatch_sync(dispatch_get_main_queue(), ^{
if ([request isEqualToString:kRequestDayInfo]) {
//formirate reply dictionary
}
});
LOG(#"send reply");
replyHandler(reply);
}
On my Watch I start load when called function in my main interface controller
- (void)willActivate {
[super willActivate];
if ([WatchConnectivityHandler instance].isConnectivityAvailable) {
[[WatchConnectivityHandler instance] loadDayInfoWithCompletion:^(NSError * _Nullable error, WatchDayInfo * _Nullable dayInfo) {
//code
}];
} else {
//error
}
}
My watch singleton
+ (nonnull instancetype)instance {
static WatchConnectivityHandler *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [WatchConnectivityHandler new];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self.isConnectivityAvailable) {
session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
return self;
}
- (BOOL)isConnectivityAvailable {
return [WCSession isSupported];
}
- (void)loadDayInfoWithCompletion:(void(^ _Nonnull)( NSError * _Nullable error, WatchDayInfo * _Nullable dayInfo))completion {
[session sendMessage:#{kRequestKey : kRequestDayInfo} replyHandler:^(NSDictionary<NSString *,id> * _Nonnull replyMessage) {
NSLog(#"reply");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(#"%#", replyMessage);
//custom code
completion(nil, /*custom object*/);
});
} errorHandler:^(NSError * _Nonnull error) {
NSLog(#"error");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(#"%#", error);
completion(error, nil);
});
}];
}
So it work fine in first time and I get reply. But then I start a lot of errors like
Error Domain=WCErrorDomain Code=7007 "WatchConnectivity session on paired device is not reachable."
Error Domain=WCErrorDomain Code=7014 "Payload could not be delivered."
I test on Watch, and it is often problem to get reply form my iPhone, it wait a long. But on iPhone I test, that when it get message from Watch, it very quick send reply, but I don't see that reply on Watch.
I need update my info every time when watch start my app. What the problem? Maybe I use not properly functions?
Use transeruserinfo method instead of sendmessage and use didreceiveuserinfo method instead of didreceiveapplicationcontext.

How to update openParentApplication for Watch OS 2?

I have a watch app that needs to communicate with the parent app to get some information. This should happen while only using the watch and the phone in a pocket. It used to work like this:
In the InterfaceController on the watch:
[InterfaceController openParentApplication:request reply:^(NSDictionary *replyInfo, NSError *error) {
// handle response from phone
}];
In the AppDelegate of the phone:
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
NSDictionary *response = // generate response
reply(response);
}
I tried to change the code in the InterfaceController to:
[[WCSession defaultSession] sendMessage:request
replyHandler:^(NSDictionary *reply) {
}
errorHandler:^(NSError *error) {
}
];
And the code in the AppDelegate to this which never seems to get called:
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *,id> *)message replyHandler:(void (^)(NSDictionary<NSString *,id> *replyMessage))replyHandler {
// this never gets called
}
I've seen examples of using sendMessage on the watch, but they all require the delegate to be in a ViewController on the phone that is open. Is there a way to get information from the parent app on the phone while the phone is not being used?
If this helps anyone, I figured out that you have to run it on the main thread.
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *,id> *)message replyHandler:(void (^)(NSDictionary<NSString *,id> *replyMessage))replyHandler {
dispatch_async(dispatch_get_main_queue(), ^{
// do stuff here
replyHandler(replyDictionary);
});
}

Resources