Hey I have made an application for my phone that can unlock/lock etc my car. Basically the iphone interface is just 4 buttons: lock, unlock, trunk, and connect. When ever I press a button a writes something to my arduino located inside of my car. I was wondering if I could "copy" these four buttons onto my apple watch. What I mean by that is that can I use openParentApplication to do that or is there some other command I could use to simulate so say button presses on my iphone when the button is clicked on my apple watch.
Code for Iphone buttons:
- (IBAction)lockCar:(UIButton *)sender;{
NSString *string = #"l";
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
if (bleShield.activePeripheral.state == CBPeripheralStateConnected) {
[bleShield write:data];
}
}
- (IBAction)trunkCar:(UIButton *)sender;{
NSString *string = #"t";
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
if (bleShield.activePeripheral.state == CBPeripheralStateConnected) {
[bleShield write:data];
}
}
- (IBAction)unlockCar:(UIButton *)sender;{
NSString *string = #"u";
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
if (bleShield.activePeripheral.state == CBPeripheralStateConnected) {
[bleShield write:data];
}
}
Code for Apple Watch so far:
- (IBAction)carConnect {
}
- (IBAction)carUnlock {
}
- (IBAction)carLock {
}
- (IBAction)carTrunk {
}
You can use WCSession to establish communication between the WatchKit extension and the companion app.
Communication channel is established as:
if ([WCSession isSupported]) {
WCSession* session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
The WCSessionDelegate must also be implemented to respond messages.
The session must be activated on each side.
Then you can send messages with sendMessage:replyHandler:errorHandler: and receive with session:didReceiveMessage:replyHandler: to execute those tasks.
I recommend reading the WatchKit Class reference and this article on Watchkit communication patterns.
Related
I have just set up WCConnectivity in my app using tutorials/sample code and I feel I have it implemented correctly. I am not using the simulator to test. For some reason, the didReceiveApplicationContext is not being called in the watch app, even though everything is set up correctly.
I've tried calling in the the Interface Controller of the WatchKit app, in the ExtensionDelegate and using NSUserDefaults to set the Interface Controller data instead.
iOS App
ViewController.m
- (void) viewDidLoad{
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
}
-(void) saveTrip{
NSMutableArray *currentTrips = [NSMutableArray arrayWithArray:[self.sharedDefaults objectForKey:#"UserLocations"]];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:newLocation];
[currentTrips addObject:data];
[self.sharedDefaults setObject:currentTrips forKey:#"UserLocations"];
[self.sharedDefaults synchronize];
WCSession *session = [WCSession defaultSession];
NSDictionary *applicationDict = [[NSDictionary alloc] initWithObjects:#[currentTrips] forKeys:#[#"UserLocations"]];;
[session updateApplicationContext:applicationDict error:nil];
}
Watch Extension Code
ExtensionDelegate.m
- (void)applicationDidFinishLaunching {
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
}
- (void)session:(nonnull WCSession *)session didReceiveApplicationContext:(nonnull NSDictionary<NSString *,id> *)applicationContext {
self.places= [applicationContext objectForKey:#"UserLocations"];
[[NSUserDefaults standardUserDefaults] setObject:self.places forKey:#"UserLocations"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
InterfaceController.m
- (void)willActivate {
[super willActivate];
self.placesData = [NSMutableArray arrayWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:#"UserLocations"]];
[self loadData];
}
For me it was two things:
Passing invalid values in the dictionary. Can't even pass NSNull() to represent nil values. If you have data in there that can't be represented in a plist, it fails.
If the dictionary doesn't change, subsequent calls to updateApplicationContext won't trigger a corresponding call to didReceiveApplicationContext. To force an update—perhaps in debug builds—you could add a UUID to the payload, e.g.
context["force_send"] = UUID().uuidString
It might prove useful to handle any errors, so change:
[session updateApplicationContext:applicationDict error:nil];
to
NSError *error;
if (![session updateApplicationContext:applicationDict error:&error]) {
NSLog(#"updateApplicationContext failed with error %#", error);
}
If it's not working, it's probably not implemented correctly.
A few issues at first glance:
In your iOS app, you get a reference to the shared session and activate it, but don't assign that local variable to any property on your view controller. This means your local variable will go out of scope, once viewDidLoad exits. You also need to correct that in your watch extension.
In saveTrip, you again create another local session, which isn't activated and doesn't have any delegate. You need to use the first session that you set up and activated earlier.
On your watch, you save data that is received but your interface controller won't know that there is new data that it should load and display.
A few tips:
If you setup and activate the session in application:didFinishLaunchingWithOptions, it will be available app-wide (and activated much earlier in your app's lifecycle).
You should check that your session is valid, as a watch may not be paired, or the watch app may not be installed. Here's a good (Swift) tutorial that covers those cases, and also uses a session manager to make it easier to support using Watch Connectivity throughout your app.
You may want to pass less/lighter data across to the watch, than trying to deal with archiving an array of custom objects.
As an aside, what you're trying to do with NSUserDefaults is very convoluted. It's really meant to persist preferences across launches. It's not appropriate to misuse it as a way to pass your model back and forth between your extension and controller.
I had the same issue and fixed.
I forgot to add WatchConnectivity framework in watch extension.
I've been through a ton of SO posts, and this USED to work, but it stopped working. I'm not sure what happened. I developed this iPhone+WatchKit app with watchOS 1.0 and everything worked fine.
I've upgraded my app, project, and Apple Watch to watchOS 2.0, and now I can't get any data via NSUserDefaults using my App Group.
App Groups is enabled in Xcode on the host App, and WatchKit Extension. I even tried turning it on for the WatchKit App as well.
My group name is called "group.com.mycompany.myapp" (with my real company name and app name) and it's selected on all of the targets.
I've confirmed the Build settings for my host app and WatchKit extension reference the entitlements files and I've checked that those entitlements files contain the app groups security option for my chosen app group.
I've made sure the bundle identifiers are different for the host app, watchkit extension, and watchkit app. I use "com.mycompany.myapp", "com.mycompany.myapp.watchkitextension", and "com.mycompany.myapp.watchkitapp", respectively.
Here are some screenshots of my Xcode config for the host app, watchkit extension, and watchkit app, just in case:
Here is the code in my host app that is able to read the data properly:
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kAppGroupName];
NSLog(#"app defaults: %#", [defaults dictionaryRepresentation]);
That produces this in my console, with kKeyEmployeeId and kKeyStoreNumber being the data that I'm trying to share between the host and watch.
2015-12-13 13:51:35.618 MyApp[27516:16126253] app defaults: {
AppleLanguages = (
"en-US",
en
);
INNextHearbeatDate = "472211882.83309";
NSInterfaceStyle = macintosh;
NSLanguages = (
"en-US",
en
);
"com.apple.content-rating.AppRating" = 1000;
"com.apple.content-rating.ExplicitBooksAllowed" = 1;
"com.apple.content-rating.ExplicitMusicPodcastsAllowed" = 1;
"com.apple.content-rating.MovieRating" = 1000;
"com.apple.content-rating.TVShowRating" = 1000;
kKeyEmployeeId = Kenny;
kKeyStoreNumber = 001;
}
In my WatchKit Extension, I have the same code, but it doesn't provide me the two key:value pairs that I need:
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kAppGroupName];
NSLog(#"defaults: %#", [defaults dictionaryRepresentation]);
}
It produces this, which is very similar to the one above, but without the two keys I really need:
2015-12-13 13:28:29.840 MyApp WatchKit Extension[720:322804] defaults: {
AppleLanguages = (
en
);
INNextHearbeatDate = "472434306.599217";
NSInterfaceStyle = macintosh;
NSLanguages = (
en
);
"com.apple.content-rating.AppRating" = 1000;
"com.apple.content-rating.ExplicitBooksAllowed" = 1;
"com.apple.content-rating.ExplicitMusicPodcastsAllowed" = 1;
"com.apple.content-rating.MovieRating" = 1000;
"com.apple.content-rating.TVShowRating" = 1000;
}
The kAppGroupName and other constants are defined like so:
NSString * const kAppGroupName = #"group.com.inadaydevelopment.MyApp";
NSString * const kKeyStoreNumber = #"kKeyStoreNumber";
NSString * const kKeyEmployeeId = #"kKeyEmployeeId";
In Watch OS1 this worked, but in Watch OS2 there has been some changes. You need to use something called WatchConnectivity to send the data you want to save to the watch. Then when the watch receives the data you sent to it, save it to the Apple watch's default NSUserDefaults.
WCSession.defaultSession() will return the WCSession singleton for transferring data between your iOS and Watch app.
Here is a tutorial and example.
Since watch os 2 apps run on the watch and not the iPhone, I don't think you have access to NSUserDefaults or app groups. You'll have to use WatchConnectivity framework to transfer data to and from the watch.
You can follow this steps
1) set the session
if ([WCSession isSupported])
{
[[WCSession defaultSession] setDelegate:self];
[[WCSession defaultSession] activateSession];
}
2) Prepare data dictionary and send it with below method
[WCSession defaultSession] sendMessage:dataDict replyHandler:^(NSDictionary<NSString *,id> * _Nonnull replyMessage) {
//You task on completion
} errorHandler:^(NSError * _Nonnull error) {
if (error)
{
//Handle the error
}
}];
3) And in watch app code
You can set delegate method
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler
{
//Handle the response
}
I have an in-home app that will be supported by IT team.
When they support desktop apps, they may read logs to find any troubles (like server returned 404 on sync request).
I can use NSLog for iOS, but how can user access them with out of Xcode?
I can't ask any user "please give me your phone to investigate what has happened".
Does there is some tool any IT person with out of Xcode and Mac may use to read iOS logs?
Does there is some tool any IT person with out of Xcode and Mac may use to read iOS logs?
Unfortunately not. It used to be that you could run an app on your device that would read the Console log, but Apple took that ability away; I guess they saw it as a security breach.
If your user can get to a Mac running Xcode, they can view the console log directly in Xcode.
Otherwise, as others have suggested, you will have to build into your app the capacity to keep a log in a place you can get to. For example you can write to a file and then offer (within the app) to email that file to yourself. Many apps have an interface to a facility like this in their Settings bundle.
I've been using the combination of CocoaLumberjack, Antenna & DDAntennalogger at work for remote logging. Basically, you have to set up an end-point at your server and Antenna will be used to send the logs remotely.
Here's the reference that when configuring it on my project:
Remote logging using CocoaLumberjack, Antenna & DDAntennaLogger
This is how you can do it:
Step 1: Redirect your NSLog statements to a text file in file system. This you can do on specific user action or always enable it and delete it periodically.
Step 2: Have a web service which will allow you to upload the saved logs in the file system. You could trigger this on user action or may be a timer based job.
Step 3: Delete the logs from file system once upload is successful.
Here is a example of such a custom logger:
#import "MyCustomLogging.h"
#define kMyCustomLoggingFile #"NSLogging.txt"
static NSString *const kMyDeviceLogUploadURL = #"uploadDeviceLogURL";
#interface MyCustomLogging ()
#property (nonatomic, strong) MyRequestHandler *requestHandler;
#property (nonatomic, assign, getter = isNsLogRedirected) BOOL nsLogRedirected;
#property (nonatomic, assign) BOOL shouldStopLogging;
#property (nonatomic, strong) NSString *pathForLogging;
#end
#implementation MyCustomLogging
static int savedStdErr = 0;
static MyCustomLogging *sharedMyCustomLogging = nil;
+ (MyCustomLogging *)sharedMyCustomLogging {
static dispatch_once_t pred = 0;
dispatch_once(&pred, ^{
sharedMyCustomLogging = [[self alloc] init];
});
return sharedMyCustomLogging;
}
#pragma mark -
#pragma mark Start Method
- (void)startLogging {
// Starting the Redirection of the Logs
if (!self.isNsLogRedirected) {
[self nsLogRedirectedToFile];
}
}
#pragma mark -
#pragma mark Stop Method
- (void)stopLogging {
NSLog(#"Stopping the logging");
NSString *aLoggingPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
self.pathForLogging = [aLoggingPath stringByAppendingPathComponent:kMyCustomLoggingFile];
// If file already exists & logging was not redirected then directly upload the logs::A Possible case of app quit/crash without uploading previous logs
if ([self isLogFilePresent] && !self.nsLogRedirected) {
[self uploadLogs];
} else if (self.isNsLogRedirected) { //Check for Successive Stop Notifications
self.shouldStopLogging = YES;
[self restoreNSLog];
} else {
NSDictionary *anUserInfo = #{kMyDeviceLogUplodStatusKey: kMyValueOne};
[[NSNotificationCenter defaultCenter] postNotificationName:kMyDeviceLogsUploadNotification object:nil userInfo:anUserInfo];
}
}
#pragma mark -
#pragma mark Private Method
- (void)nsLogRedirectedToFile {
if (!self.isNsLogRedirected) {
NSLog(#"Redirecting NSLogs to a file.....");
self.nsLogRedirected = YES;
savedStdErr = dup(STDERR_FILENO);
NSString *aLoggingPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
self.pathForLogging = [aLoggingPath stringByAppendingPathComponent:kMyCustomLoggingFile];
NSLog(#"Logging Path: %#", self.pathForLogging);
freopen([self.pathForLogging cStringUsingEncoding:NSASCIIStringEncoding],"a+",stderr);
NSLog(#"NSLog Redirected to a file Succesfully");
[MySessionObject setLoggingOn:YES];
}
}
- (void)restoreNSLog {
if (self.isNsLogRedirected) {
[MySessionObject setLoggingOn:NO];
NSLog(#"NSLog Will be Restored now....");
self.nsLogRedirected = NO;
fflush(stderr);
dup2(savedStdErr, STDERR_FILENO);
close(savedStdErr);
savedStdErr = 0;
}
[self uploadLogs];
NSLog(#"NSLog Restored Successfully");
}
- (void)uploadLogs {
NSLog(#"Now uploading files");
// Disable logging before files are uploading
MySessionObject.enableLogging = NO;
NSError *anError = nil;
NSData *aLogData = [NSData dataWithContentsOfFile:self.pathForLogging options:NSDataReadingUncached error:&anError];
//Converting to String
NSString *aLogString = [[NSString alloc] initWithData:aLogData encoding:NSUTF8StringEncoding];
NSMutableDictionary *aPostBody = [[NSMutableDictionary alloc] initWithCapacity:3];
[aPostBody setValue:aLogString forKey:#"logData"];
[aPostBody setValue:MySessionObject.wifiMACAddress forKey:#"deviceMACAddress"];
[aPostBody setValue:MySessionObject.deviceToken forKey:#"deviceID"];
__weak MyCustomLogging *aBlockSelf = self;
self.requestHandler = [[MyRequestHandler alloc] initWithEndPoint:#"/uploadLogs" body:aPostBody container:nil loadingOverlayTitle:nil successHandler:^(NSDictionary *iResponse) {
if (iResponse) {
//Remove the File From the Path
NSError *aFileError = nil;
BOOL aFileRemoveSuccess = [[NSFileManager defaultManager] removeItemAtPath:self.pathForLogging error:&aFileError];
if (!aFileRemoveSuccess) {
//Tracking the Event
NSString *aDescription = [NSString stringWithFormat:#"Error Code:%ld Error Description:%#", (long)[aFileError code], [aFileError localizedDescription]];
NSLog(#"Error occured while deleting log file:%#", aDescription);
}
// Clearing all
aBlockSelf.pathForLogging = nil;
NSDictionary *anUserInfo = #{kMyDeviceLogUplodStatusKey: kMyValueOne};
[[NSNotificationCenter defaultCenter] postNotificationName:kMyDeviceLogsUploadNotification object:nil userInfo:anUserInfo];
}
} andErrorHandler:^(NSString *iMessage, NSString *iKey, NSInteger iErrorCode, BOOL iIsNetworkError) {
NSDictionary *anUserInfo = #{kMyDeviceLogUplodStatusKey: kMyValueZero};
[[NSNotificationCenter defaultCenter] postNotificationName:kMyDeviceLogsUploadNotification object:nil userInfo:anUserInfo];
}];
[self.requestHandler executeRequest];
}
- (BOOL)isLogFilePresent {
NSFileManager *aFileManager = [[NSFileManager alloc] init];
BOOL aFilePresent = [aFileManager fileExistsAtPath:self.pathForLogging];
return aFilePresent;
}
#end
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");
}
}
I am working on an apple watch app, but facing a weird issue, i.e. my watch app works only when I manually open the iPhone app or when iPhone app is in background. When I terminate my iPhone app and test apple watch app then it does not work any more.
Here I am mentioning watch app flow:
When apple watch app starts, I call a web api to fetch response from server.
I used openParentApplication:reply: method to call web api from parent app
I understand that, I will have to call web api method in a background thread because, openParentApplication:reply: method automatically open the parent app in iPhone and suspends in a mean time, so if we are processing a time taking task using this method then we should use a background thread as mentioned under WatchKit Development Tips. So I am using a background thread to call web api.
When I get response I pass it to watch app.
Here is the attached snippet:
Watch App - InitialInterfaceController.m
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
}
- (void)willActivate {
[super willActivate];
[self getDetails];
}
- (void)getDetails{
//Open parent app
[WKInterfaceController openParentApplication:#{#“request”:#“details”}
reply:^(NSDictionary *replyInfo, NSError *error) {
if (!error) {
NSLog(#“Success”);
[self parseKPI:replyInfo];
}
else{
NSLog(#"Error - %#", error.localizedDescription);
}
}];
}
iPhone App - AppDelegate.m
- (void)application:(UIApplication *)application
handleWatchKitExtensionRequest:(NSDictionary *)userInfo
reply:(void (^)(NSDictionary *))reply{
NSString *request = [userInfo objectForKey:#“request”];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
// Get Details
if ([request isEqualToString:#"details"]) {
APIHandler *api = [[APIHandler alloc] init];
[api getDetailsUsername:#“my_user_name”
onSuccess:^(NSDictionary *details) {
dispatch_async(dispatch_get_main_queue(), ^{
reply(details);
});
} onFailure:^(NSString *message) {
dispatch_async(dispatch_get_main_queue(), ^{
reply(#{#"Error":message});
});
}];
}
}
iPhone App - APIHandler.m
- (void) getDetailsUsername:(NSString *)username
onSuccess:(void(^)(NSDictionary * details))success
onFailure:(void(^)(NSString *message))failure{
NSString *urlString = [NSString stringWithFormat:#"%#%#", HOST, DETAILS_API];
urlString = [urlString stringByAppendingFormat:#"?username=%#",username];
urlString = [urlString stringByAppendingFormat:#"&%#", self.APIKeyParameter];
NSURL *url = [NSURL URLWithString:urlString];
NSMutableURLRequest *mutableURL = [NSMutableURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:mutableURL
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (!connectionError) {
NSError *error = nil;
NSDictionary *details = [NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableLeaves
error:&error];
success(details);
}
else{
failure(#"Connection Error!");
}
}];
}
But this approach is not working for me.
I found one more issue in watch app simulator i.e.
- (void)awakeWithContext:(id)context for my initial view controller is called, but - (void)willActivate method is not being called sometimes and I just see watch app spinner. Some times it works. It’s quite strange. I have around 15 controls (including all groups) in initial interface controller added using storyboard.
I have also referred Watchkit not calling willActivate method and modified my code but still facing same issue.
Can any one let me know why is this issue persisting in my app?
You'll need to handle the request in the iPhone app a bit differently in order for your app to not be killed off by the OS. I've shared a similar answer here: https://stackoverflow.com/a/29848521/3704092