I'm trying to make a simple watch app that displays information sent by the companion app. I'm having some trouble getting the WatchKit app to properly receive the information.
On the sender side I have the following code:
- (void)viewDidLoad {
[super viewDidLoad];
// Prevents UIWebView from displaying under nav bar
self.navigationController.navigationBar.translucent = NO;
_timer = [NSTimer scheduledTimerWithTimeInterval:12.0 target:self selector:#selector(showAlert) userInfo:nil repeats:NO];
_diningLocations = [[NSMutableArray alloc] initWithCapacity:0];
if ([WCSession isSupported]) {
self.session = [WCSession defaultSession];
self.session.delegate = self;
[self.session activateSession];
// The following line works
//[self.session updateApplicationContext:#{#"hello" : #"world"} error:nil];
}
- (void)grabLocationsFromServer {
_query = [PFQuery queryWithClassName:self.tableName];
[MBProgressHUD showHUDAddedTo:self.view animated:YES];
[_query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// Do stuff
[self.locationTable reloadData];
[self loadWatchApp];
} else {
// Log details of the failure
NSLog(#"Error: %# %#", error, [error userInfo]);
[MBProgressHUD hideHUDForView:self.view animated:YES];
}
[MBProgressHUD hideHUDForView:self.view animated:YES];
}];
}
- (void)loadWatchApp {
if(self.session) {
if (self.session.paired && self.session.watchAppInstalled) {
NSError *error;
[self.session updateApplicationContext:#{#"Locations": self.diningLocations} error:&error];
}
}
}
On the receiving end I have this simple code snippet:
func loadTableData(data: [RFDiningLocation]) {
table.setNumberOfRows(data.count, withRowType: "location")
for i in 0..<data.count {
let row = table.rowControllerAtIndex(i) as! RFDiningRowController
// row.label.setText(data[i].name)
row.label.setText("Hello, world!")
}
}
When I call updateApplicationContext in viewDidLoad, it works 100% fine, but when I call it from within another function the WatchKit app just doesn't respond. I can confirm that loadWatchApp is called as well as updateApplicationContext. Thanks!
I would implement the session function from WCSessionDelegate activationDidCompleteWithState and then in the callback if state is activated then call your grabLocationsFromServer function. If grabLocationsFromServer gets called before you activate the Watch session, that could be why your application context isn't being updated.
Also, it's suggested that you call activate session as soon as possible, such as in the init of the App Delegate.
I was just experimenting before you said this worked for you, so here's what I'm seeing:
On startup of watch (initial connection), there is a call to activationDidCompleteWithState but there is no context yet, so didReceiveApplicationContext is not called.
Watch app updates per expected applicationContext only occurs when I send an empty message in sessionReachabilityDidChange. My understanding is that I have to "awake" the phone app in the background by sending this initial message. This message can be ignored on the phone side.
func sessionReachabilityDidChange(session: WCSession) {
if session.reachable {
sendMessage(["InitialMessage": true], replyHandler: nil, errorHandler: nil)
}
}
Related
I have a 'Terms and Conditions' controller I represent in the first launch of the app, but when opening the app in the first time from the App Store page the Terms and Conditions' controller not shown- only after I close the app and reopen it from the device itself (not the App Store page) then the controller is shown.
this code is from the launched screen controller:
- (void)viewDidLoad {
[self agreedToServerTerms];
}
- (void)agreedToServerTerms {
[[HttpUtils instance] httpRequest:TERMS_AGREEMENT_URL :params completionHandler:^(NSData *data, NSError *error) {
#try {
bool acceptTerms = false;
if (error) {
[Utils log:[NSString stringWithFormat:#"agreedToServerTerms error=%#", error]];
} else {
NSDictionary *jsonData = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!error) {
acceptTerms = [jsonData.allValues[0] boolValue];
if (!acceptTerms) {
TermsAndConditionController *tac = [[TermsAndConditionController alloc] initWithNibName:#"TermsAndConditionController" bundle:nil];
tac.delegate = self;
[[SlideNavigationController sharedInstance] pushViewController:tac animated:false];
}
else {
[self performSelectorInBackground:#selector(initialApp) withObject:nil];
}
} else {
[Utils log:[NSString stringWithFormat:#"parsing jsonData error = %#" ,error]];
}
}
} #catch (NSException *e) {
[Utils log:[NSString stringWithFormat:#"Exception occurred: %#, %#", e, [e userInfo]]];
}
}];
}
from App delegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
LaunchScreenController *bbp = [[LaunchScreenController alloc] initWithNibName:#"LaunchScreenController" bundle:nil];
[[SlideNavigationController sharedInstance] pushViewController:bbp animated:YES];
if (_launchedURL) {
bbp.launchedURL = _launchedURL;
}
[self.window addSubview:bbp.view];
}
herewith how I do almost the exact same thing. I think you need to rework yours, it could be a bit different, but this may help.
In the first VC that my app displays, in its viewWillAppear message, I have the following code
// Terms of use
if ( [NSUserDefaults.standardUserDefaults integerForKey:#"tou"] != 20170614 )
{
[self performSegueWithIdentifier:#"tou" sender:nil];
[NSUserDefaults.standardUserDefaults setInteger:20170614 forKey:#"tou"];
}
This uses user defaults to note if the terms of use have ever been presented and, if so, it marks #"tou" with some arbitrary value. In your case you can set the logic in your controller and only mark it once the user accepts.
This requires a segue in the storyboard called #"tou" that will present your T&C controller and you may need to prevent exit if the user does not agree, but the idea is to segue away from your first VC if the user did not agree yet to present the user with the T&C rather than injecting it into the app delegate as you do at present.
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.
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.
I have a problem with the Facebook SDK in iOS. Everything works perfectly but I have a problem with the login flow. When the user hits the "log into Facebook" button he gets directed to Facebook to allow my app to get permissions. That's fine. But when iOS is changing from the browser to my app and trying to make a request I get this error message:
FBSDKLog: Error for request to endpoint 'search?q=concert&type=event&limit=10': An open FBSession must be specified for calls to this endpoint.
When I'm closing the app and start it again and go and execute my request it works without error. I get my reqeust. That´s my code I know its not very neat.
ViewdidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
//Some other Code
if ([[FBSession activeSession]state])
{
NSLog(#"User is logged in");
[loginView setHidden:TRUE];
} else {
// try to open session with existing valid token
NSArray *permissions = [[NSArray alloc] initWithObjects:
#"user_events",
nil];
FBSession *session = [[FBSession alloc] initWithPermissions:permissions];
[FBSession setActiveSession:session];
if([FBSession openActiveSessionWithAllowLoginUI:NO]) {
} else {
NSLog(#"User is logged out");
//Create the FB Login-Button
loginView = [[FBLoginView alloc] initWithReadPermissions:#[#"user_events"]];
// Align the button in the center horizontally
loginView.frame = CGRectOffset(loginView.frame, (self.view.center.x - (loginView.frame.size.width / 2)), 200);
[self.view addSubview:loginView];
}
}
[self getEvents];
}
getEvents:
-(void)getEvents{
//Get General Events
[FBRequestConnection startWithGraphPath:#"search?q=concert&type=event&limit=10"
completionHandler:^(FBRequestConnection *connection, id result, NSError *error) {
if (!error) {
//Some Code
else {
// An error occurred, we need to handle the error
// See: https://developers.facebook.com/docs/ios/errors
}
}];
}
appDidEnterForeground
- (void)appDidEnterForeground:(NSNotification *)notification {
NSLog(#"App did enter forground again.");
[self viewDidLoad];
}
ok, well I solved my own problem: I read through the docs again extremely carefully did everything again, added this to my delegate:
// Whenever a person opens the app, check for a cached session
if (FBSession.activeSession.state == FBSessionStateCreatedTokenLoaded) {
// If there's one, just open the session silently, without showing the user the login UI
[FBSession openActiveSessionWithReadPermissions:#[#"public_profile"]
allowLoginUI:NO
completionHandler:^(FBSession *session, FBSessionState state, NSError *error) {
// Handler for session state changes
// This method will be called EACH time the session state changes,
// also for intermediate states and NOT just when the session open
[self sessionStateChanged:session state:state error:error];
}];
}
}
// This method will handle ALL the session state changes in the app
-(void)sessionStateChanged:(FBSession *)session state:(FBSessionState) state error: (NSError *)error
{
// If the session was opened successfully
// customize your code...
}
and then I´m executing my method, which contains my request like this
// Logged-in user experience
- (void)loginViewShowingLoggedInUser:(FBLoginView *)loginView {
NSLog(#"You're logged in. (Method: loginviewshowingloggedinuser");
//If the User is logged in we can fetch the events
[self getEvents];
}
Hi i Intigrate SpotifyLib CocoaLibSpotify iOS Library 17-20-26-630 into my Project. I open its SPLoginViewController using Bellow Method:-
-(void)OpenSpotify
{
NSError *error = nil;
[SPSession initializeSharedSessionWithApplicationKey:[NSData dataWithBytes:&g_appkey length:g_appkey_size]
userAgent:#"com.mycomp.spotify"
loadingPolicy:SPAsyncLoadingImmediate
error:&error];
if (error != nil) {
NSLog(#"CocoaLibSpotify init failed: %#", error);
abort();
}
[[SPSession sharedSession] setDelegate:self];
[self performSelector:#selector(showLogin) withObject:nil afterDelay:0.0];
}
-(void)showLogin
{
SPLoginViewController *controller = [SPLoginViewController loginControllerForSession:[SPSession sharedSession]];
controller.allowsCancel = YES;
//controller.view.frame=;
[self presentViewController:controller animated:YES completion:nil];
}
At First time that Appear Spotify Login Screen. After that I tap On Cancel Button, and Try to open again login screen then i got crash EXC_BAD_EXE at this line. sp_error createErrorCode = sp_session_create(&config, &_session);
UPDATE
I Found exet where is got BAD_EXC
in this method
+(void)dispatchToLibSpotifyThread:(dispatch_block_t)block waitUntilDone:(BOOL)wait {
NSLock *waitingLock = nil;
if (wait) waitingLock = [NSLock new];
// Make sure we only queue one thing at a time, and only
// when the runloop is ready for it.
[runloopReadyLock lockWhenCondition:1];
CFRunLoopPerformBlock(libspotify_runloop, kCFRunLoopDefaultMode, ^() {
[waitingLock lock];
if (block) { #autoreleasepool { block(); } }
[waitingLock unlock];
});
if (CFRunLoopIsWaiting(libspotify_runloop)) {
CFRunLoopSourceSignal(libspotify_runloop_source);
CFRunLoopWakeUp(libspotify_runloop);
}
[runloopReadyLock unlock]; // at hear when my debug poin reach after pass this i got bad_exc
if (wait) {
[waitingLock lock];
[waitingLock unlock];
}
}
after doing lots of search i got Solution i check that whether the session already exists then i put if condition like:-
-(void)OpenSpotify
{
SPSession *session = [SPSession sharedSession];
if (!session) {
NSError *error = nil;
[SPSession initializeSharedSessionWithApplicationKey:[NSData dataWithBytes:&g_appkey length:g_appkey_size]
userAgent:#"com.mycomp.spotify"
loadingPolicy:SPAsyncLoadingImmediate
error:&error];
if (error != nil) {
NSLog(#"CocoaLibSpotify init failed: %#", error);
abort();
}
[[SPSession sharedSession] setDelegate:self];
}
[self performSelector:#selector(showLogin) withObject:nil afterDelay:0.0];
}
-(void)showLogin
{
SPLoginViewController *controller = [SPLoginViewController loginControllerForSession:[SPSession sharedSession]];
controller.allowsCancel = YES;
[self presentViewController:controller animated:YES completion:nil];
}
Now no crash and working fine.