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);
});
}
Related
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?
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.
as apple suggested use Handoff in Glance .
I wants to call web API in Glance Interface , for this I did following things
- (void)awakeWithContext:(id)context
{
[super awakeWithContext:context];
[self CreateUaerActivity];
}
-(void)CreateUaerActivity
{
NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:#"com.xxx.xxx.glance"];
activity.title = #"Glance";
activity.delegate=self;
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:kUserLoginWatchKit,kRequestTypeWatchKit, nil];
activity.userInfo = dict;
self.userActivity = activity;
[self.userActivity becomeCurrent];
}
- (void)willActivate
{
[super willActivate];
[NSTimer scheduledTimerWithTimeInterval:120 target:self selector:#selector(doSomething) userInfo:nil repeats:YES];
}
-(void)doSomething
{
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:kUserLoginWatchKit,kRequestTypeWatchKit, nil];
[super updateUserActivity:#"com.xxx.xxx.glance" userInfo:dict webpageURL:nil];
}
-(void)handleUserActivity:(NSDictionary *)userInfo
{
//displaying data
}
and in AppDelegate.m file -
-(BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler
{
NSLog(#"Handoff dictionary: %#", userActivity.userInfo);
NSString *requestType = userActivity.userInfo[kRequestTypeWatchKit];
if ([requestType isEqual: kGlanceDataWatchKit])
{
//calling web API to get Data
}
return YES;
}
I found AppDelegate never called continueUserActivity method to return something to Glance interface.
please guide me how to call API through Glance Interface.
I'm not sure if this is what you want, but if you want to call an web Api i suggest yout to do it like this :
in the GlanceInterfaceController :
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
[dictionary setObject:#"getSomething" forKey:#"action"];
[MainInterfaceController openParentApplication:dictionary reply:^(NSDictionary *replyInfo, NSError *error) {
NSLog(#"Reply received by Watch app: %#", replyInfo); // the reply from the appDelegate...
}
in your parent's app Delegate :
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
{
NSLog(#"Request received by iOS app");
if( [userInfo objectForKey:#"action"] isEqualToString:#"getSomething"] ){
//call you're Web API
//send the reponse to you're glance :
reply(DictResponse);// some Dictionary from your web API...
}
*****EDIT*****
i've been issued the same issue, one easy fix is to begin an background task, from :
fiveminutewatchkit
Here's the way :
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
{
// Temporary fix, I hope.
// --------------------
__block UIBackgroundTaskIdentifier bogusWorkaroundTask;
bogusWorkaroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:bogusWorkaroundTask];
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] endBackgroundTask:bogusWorkaroundTask];
});
// --------------------
__block UIBackgroundTaskIdentifier realBackgroundTask;
realBackgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
reply(nil);
[[UIApplication sharedApplication] endBackgroundTask:realBackgroundTask];
}];
// Kick off a network request, heavy processing work, etc.
// Return any data you need to, obviously.
reply(nil);
[[UIApplication sharedApplication] endBackgroundTask:realBackgroundTask];
}
in fact iOS kill your parent's app before you can retrieve data... this (not very clean solution) prevent you're app to be killed... and let you the time to retrieve infos...
******END EDIT******
I am not sure where exactly my question fit. Here is the issue :
I want the push notifications to be registered on application start. For which I am registering in AppDelegate didFinishLaunchingWithOptions.
sem = dispatch_semaphore_create(0);
[manager registerForPushNotifications];
dispatch_semaphore_signal(sem);
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
If I don't use GCD, return YES; of didFinishLaunchingWithOptions is called first and in that case, my my service method, which want to call from didRegisterForRemoteNotificationsWithDeviceToken for sending device token is not called.
// system push notification registration success callback, delegate to pushManager
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
NSString *token = [[deviceToken description] stringByTrimmingCharactersInSet: [NSCharacterSet characterSetWithCharactersInString:#"<>"]];
token = [token stringByReplacingOccurrencesOfString:#" " withString:#""];
NSLog(#"content---%#", token);
[[NSUserDefaults standardUserDefaults]setObject:token forKey:#"deviceToken"];
[self registerForPushWooshNotification];
[[PushNotificationManager pushManager] handlePushRegistration:deviceToken];
}
-(void)registerForPushWooshNotification
{
NSDictionary *params = #{#"TokenId": [[NSUserDefaults standardUserDefaults]objectForKey:#"deviceToken"]
};
[_sharedHandler.requestManager POST:TGURL_PUSHWOOSH_NOTIFICATION
parameters:params
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSError *e;
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:[operation.responseString dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:&e];
NSLog(#"------ Registered for Pushwoosh ------");
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
}
But as I have implemented the GCD, didRegisterForRemoteNotificationsWithDeviceToken never gets called.
Summary :
1. I have to register application on app start.
2. Web Service needs to be called on app start.
3. If GCD not used : return YES; is called first, and didRegisterForRemoteNotificationsWithDeviceToken gets called after delay.
4. If GCD used : didRegisterForRemoteNotificationsWithDeviceToken is never called.
I had searched for didFinishLaunchingWithOptions wait and didRegisterForRemoteNotificationsWithDeviceToken called after delay on google before posting this question but no success.
The problem here is that by waiting on the semaphore, you are blocking the main thread, and the callback you are expecting to happen later will be delivered on the main thread. Since you're blocking the main thread, the callback will never happen. If you want your app to not do anything until that callback is received, you have to set that up another way.
The solution that would be philosophically closest to what you have now would be to spin the main run loop while waiting waiting for the callback, but there are a number of different ways to do it, and that's probably not the way I would choose. That said, if you wanted to do it that way, it might look something like this:
#implementation AppDelegate
{
BOOL didRegisterCalled;
NSData* token;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[application registerForRemoteNotifications];
NSLog(#"registerForRemoteNotifications called. waiting for callback.");
while (!didRegisterCalled)
{
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
}
NSLog(#"Register call back happened, and execution resumed");
return YES;
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
didRegisterCalled = YES;
token = [deviceToken copy];
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
didRegisterCalled = YES;
token = nil;
}
#end
The simplest way would be to split your startup tasks into a separate method and have the callback call that method. (FWIW, this is probably the approach I would choose.) That might look like this:
#implementation AppDelegate
{
BOOL didRegisterCalled;
NSData* token;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSLog(#"Calling registerForRemoteNotifications and deferring the rest of app startup.");
[application registerForRemoteNotifications];
return YES;
}
- (void)theRestOfTheAppStartupProcess
{
NSLog(#"Finishing app startup now that registration has happened.");
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
token = [deviceToken copy];
if (!didRegisterCalled)
{
didRegisterCalled = YES;
[self theRestOfTheAppStartupProcess];
}
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
token = nil;
if (!didRegisterCalled)
{
didRegisterCalled = YES;
[self theRestOfTheAppStartupProcess];
}
}
#end
Another way might be to set up a dispatch queue, suspend it, and then have the callback resume it. That might look like this:
#implementation AppDelegate
{
dispatch_queue_t appStartupQueue;
NSData* token;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
appStartupQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
dispatch_suspend(appStartupQueue);
dispatch_set_target_queue(appStartupQueue, dispatch_get_main_queue());
dispatch_async(appStartupQueue, ^{
[self theRestOfTheAppStartupProcess];
});
NSLog(#"Calling registerForRemoteNotifications and deferring the rest of app startup.");
[application registerForRemoteNotifications];
return YES;
}
- (void)theRestOfTheAppStartupProcess
{
NSLog(#"Finishing app startup now that registration has happened.");
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
token = [deviceToken copy];
if (appStartupQueue)
{
dispatch_resume(appStartupQueue);
appStartupQueue = nil;
}
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
token = nil;
if (appStartupQueue)
{
dispatch_resume(appStartupQueue);
appStartupQueue = nil;
}
}
#end
But what you have currently is going to produce a deadlock.
I referred below url and try to solve the issue but still not able to fix the issue.
Please help me out.
Dropbox SDK 401 Error
My app is working in iPhone 6 but while running the same app in iPhone 5 or 5s its showing the error:
[WARNING] DropboxSDK: error making request to /1/metadata/dropbox/ALLCREW.TXT - (401) No auth method found.
2014-12-11 16:58:14.628 user_schedule_3[2331:112832] Error loading metadata: Error Domain=dropbox.com Code=401 "The operation couldn’t be completed. (dropbox.com error 401.)" UserInfo=0x7fbb50d851c0 {path=/ALLCREW.TXT, error=No auth method found.}.
the code is given below:
AppDelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions
{
// Override point for customization after application launch.
DBSession *dbSession = [[DBSession alloc]
initWithAppKey:#"******h4xl9l4o"
appSecret:#"*******1ujh8"
root:kDBRootDropbox]; // either kDBRootAppFolder or kDBRootDropbox
[DBSession setSharedSession:dbSession];
// NSString *listValue = #"NAME";
return YES;
}
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url
sourceApplication:(NSString *)source annotation:(id)annotation {
if ([[DBSession sharedSession] handleOpenURL:url]) {
if ([[DBSession sharedSession] isLinked]) {
NSLog(#"dropbox linked successfully!");
[[NSNotificationCenter defaultCenter] postNotificationName:#"updateRoot" object:nil];
NSLog(#"came out");
// At this point you can start making API calls
}
return YES;
}
// Add whatever other url handling code your app requires here
return NO;
}
View controller:
- (IBAction)didPressLink {
if (![[DBSession sharedSession] isLinked]) {
[[DBSession sharedSession] linkFromController:self];
NSLog(#"did press link is linked");
} else {
NSLog(#"did press link is reached");
}
}
NSString *filename = #"ALLCREW.TXT";
NSString *localDir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *localPath = [localDir stringByAppendingPathComponent:filename];
[self.restClient loadMetadata:#"/ALLCREW.TXT"];
//[self.restClient loadFile:#"/ALLCREW.TXT" intoPath:localPath];
NSString *contentOfFile = [NSString stringWithContentsOfFile:localPath encoding:NSUTF8StringEncoding error:nil];
NSArray *stringWithEnter = [contentOfFile componentsSeparatedByString: #"\n"];
- (void)restClient:(DBRestClient *)client
loadMetadataFailedWithError:(NSError *)error {
NSLog(#"Error loading metadata: %#", error);
}
- (void)restClient:(DBRestClient *)client loadedFile:(NSString *)localPath
contentType:(NSString *)contentType metadata:(DBMetadata *)metadata {
NSLog(#"File loaded into path: %#", localPath);
[self getEntry ];
[self.tableView reloadData];
[spinner stopAnimating];
self.navigationItem.rightBarButtonItem.enabled = true;
}
- (void)restClient:(DBRestClient *)client loadFileFailedWithError:(NSError *)error {
NSLog(#"There was an error loading the file: %#", error);
}
I had the same issue. The solution ended up being that you had to add:
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
if ([[DBSession sharedSession] handleOpenURL:url]) {
if ([[DBSession sharedSession] isLinked]) {
NSLog(#"App linked successfully!");
// At this point you can start making API calls
}
return YES;
}
// Add whatever other url handling code your app requires here
return NO;
}
You seem to have this code in:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url sourceApplication:(NSString *)source annotation:(id)annotation;
Moving the code to handleOpenURL: should fix the issue.
Also make sure to link Security.framework!
Source: http://innofied.com/integration-of-dropbox-in-ios-applications/