Understanding handleWatchKitExtensionRequest - ios

I am testing the execution of some code on the iPhone app. I follow the documentation that Apple suggests (without using background tasks, just a console log). However I do not get anything on the console (I'd like to see the string "howdy").
Is this because I am running the WatchKit Extension app on the simulator? Or is there something that I am missing?
Apple says:
If you are using openParentApplication:reply:, make sure you create a
background task immediately upon entering
application:handleWatchKitExtensionRequest:reply:. This will make sure
that the iPhone app gets time in the background instead of being
suspended again. Additionally, wrap the call to endBackgroundTask: in
a dispatch_after of 2 seconds to ensure that the iPhone app has time
to send the reply before being suspended again.
My implementation on the WatchKit extension (action method linked to a button):
- (IBAction)sendMessageToApp{
NSString *requestString = [NSString stringWithFormat:#"executeMethodA"]; // This string is arbitrary, just must match here and at the iPhone side of the implementation.
NSDictionary *applicationData = [[NSDictionary alloc] initWithObjects:#[requestString] forKeys:#[#"theRequestString"]];
[WKInterfaceController openParentApplication:applicationData reply:^(NSDictionary *replyInfo, NSError *error) {
NSLog(#"\nReply info: %#\nError: %#",replyInfo, error);
}];
NSLog(#"sending message..");
}
My implementation on the AppDelegate.m file:
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply {
NSString * request = [userInfo objectForKey:#"requestString"];
NSLog(#"howdy");
// This is just an example of what you could return. The one requirement is
// you do have to execute the reply block, even if it is just to 'reply(nil)'.
// All of the objects in the dictionary [must be serializable to a property list file][3].
// If necessary, you can covert other objects to NSData blobs first.
NSArray * objects = [[NSArray alloc] initWithObjects:[NSDate date],[NSDate date],[NSDate date], [NSDate date], nil];
NSArray * keys = [[NSArray alloc] initWithObjects:#"objectAName", #"objectdName", #"objectBName", #"objectCName", nil];
NSDictionary * replyContent = [[NSDictionary alloc] initWithObjects:objects forKeys:keys];
reply(replyContent);
}
What I get in the console:
2015-04-16 14:43:44.034 FanLink WatchKit Extension[3324:142904] <InterfaceController: 0x608000081fe0> initWithContext
2015-04-16 14:44:04.848 FanLink WatchKit Extension[3324:142904] sennding message..
2015-04-16 14:44:04.854 FanLink WatchKit Extension[3324:142904]
Reply info: {
objectAName = "2015-04-16 13:44:04 +0000";
objectBName = "2015-04-16 13:44:04 +0000";
objectCName = "2015-04-16 13:44:04 +0000";
objectdName = "2015-04-16 13:44:04 +0000";
}
Error: (null)

This is because you are only debugging the WatchKit extension target. If you wanted to see in the console your main application logs as well you will need to attach the main application's process through the Xcode debugger.
Go to Debug-->Attach a process-->(Then select by identifier and type you applications name)
For a better walk through I found this great resource for you specially for WatchKit/WatchKit extension debugging:
https://mkswap.net/m/blog/How+to+debug+an+iOS+app+while+the+WatchKit+app+is+currently+running+in+the+simulator

Your console logs are correct because you are currently debugging only WatchKit Extension target So you will receive logs which are written in WatchKit extension. NSLog written in iOS App will not be printed in console.

don't know if off topic but there is a simple mistake in the code above
in watch kit there is initWithObjects:#[requestString] forKeys:#[#"theRequestString"]
and in appdelegate NSString * request = [userInfo objectForKey:#"requestString"];
"theRequestString" do not corespondent to "requestString"
took me some time to figure out why my function doesn't fire up
thanks for the code it helped

Related

How to get use BGTask in objective-c with iOS 13(Background Fetch)

So i want to build a iOS, i am pretty new to the world of objective-c and one feature i want to implement is the ability to send a API request and do a bit of background processing while the app is not "in focus/in background". I have researched for a couple days about this BGTask API for iOS 13 and have created a projected to see if i can get "background fetch" working. I have not be able to. Im pretty sure i have everything setup correctly but i can not get background fetch functionality to trigger on my iPhone, not even once over the past couple days.
I am using a actual iOS device to test this with iOS 13.4.1
"Permitted background task scheduler identifiers" is setup properly in Info.plist
App is signed
Background processing and Background fetch is checked in Background Modes
I waited the 15 minute interval as per Apples documentation
Here is my code. All this is just a blank iOS project using objective-c. I only edited AppDelegate.m and Info.plist
AppDelegate.m
#import "AppDelegate.h"
#import <BackgroundTasks/BackgroundTasks.h>
static NSString* TaskID = #"com.myapp.task";
#interface AppDelegate ()
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:TaskID
usingQueue:nil
launchHandler:^(BGProcessingTask *task) {
[self handleAppRefreshTask:task];
}];
return YES;
}
#pragma mark - UISceneSession lifecycle
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return [[UISceneConfiguration alloc] initWithName:#"Default Configuration" sessionRole:connectingSceneSession.role];
}
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
-(void)handleAppRefreshTask:(BGProcessingTask *)task {
//do things with task
NSLog(#"Process started!");
task.expirationHandler = ^{
NSLog(#"WARNING: expired before finish was executed.");
};
NSString *targetUrl = #"https://webhook.site/1b274a6f-016f-4edf-8e31-4ed7058eaeac";
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setHTTPMethod:#"GET"];
[request setURL:[NSURL URLWithString:targetUrl]];
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:
^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
NSString *myString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Data received: %#", myString);
}] resume];
task.expirationHandler = ^{
NSLog(#"WARNING: expired before finish was executed.");
};
[task setTaskCompletedWithSuccess:YES];
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
NSLog(#"Entering background");
BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:TaskID];
request.requiresNetworkConnectivity = true;
request.requiresExternalPower = false;
request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:60];
#try {
[[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:nil];
}
#catch(NSException *e){
NSLog(#" Unable to submit request");
}
}
#end
Is background fetch broken in iOS 13? Even clicking on the “Simulate background fetch" in Xcode debug menu does not work. It just closes the app and nothing happens. Can anybody help/give any advice?
A few observations:
The setTaskCompletedWithSuccess should be inside the network request’s completion handler. You don’t want to mark the task as complete until the request has had a chance to run and you’ve processed the result.
You are calling submitTaskRequest, but passing nil for the NSError reference. You have also wrapped that in an exception handler. But this API call doesn’t throw exceptions, but rather just passes back errors. But you have to supply it an error reference. E.g.
NSLog(#"Entering background");
BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:TaskID];
request.requiresNetworkConnectivity = true;
request.requiresExternalPower = false;
request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:60];
NSError *error;
if (![[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]) {
NSLog(#"BGTaskScheduler failed: %#", error);
}
In your code, if it failed, you would never know.
You have placed this code in applicationDidEnterBackground. I.e., are you seeing this “Entering background” message at all? The reason I ask is that if you’ve supplied a scene delegate (common if you just created a new iOS 13 app), this method won’t be called, whereas sceneDidEnterBackground will.
You said that you tried “Simulate background fetch”. But you haven’t created a background fetch request (a BGAppRefreshTask). You created a background task (a BGProcessingTask), which is a different thing. To test background processing requests, refer to Starting and Terminating Tasks During Development.
There’s an interesting question as to how you know that the fetch request was processed. You’re just using NSLog (which presumes that you’re keeping your app attached to the Xcode debugger). I would suggest testing this without the app being attached to Xcode. There are a few options:
If you can watch your server logs for requests, that works.
I personally will often put in UserNotifications (and make sure to go into settings and turn on persistent notifications so I don’t miss them).
Another approach that I’ve done is to log these events in some table in my app’s persistent storage and then have some UI within the app to fetch this data so I can confirm what happened.
I’ll often use Unified Logging so that I can watch os_log statements issued by my device from the macOS Console even when Xcode is not running. This is very useful in logging app/scene methods. See WWDC 2016 Unified Logging and Activity Tracing
Whatever you do, for things like background processing, background app refresh, etc., I will program some mechanism so that I can check to see if the requests/tasks took place, even when not attached to Xcode. Being attached to the debugger can, in some cases, affect the app lifecycle, and I want to make sure I’ve got some way to confirm what was going on without the benefit of the console.
Likely obvious, but make sure you never “force quit” the app, as that will stop background processes from taking place.
For more information, See WWDC 2019 video Advances in App Background Execution.

How to get last seen page analytics?

Some users of my app are unexpectedly quitting the app before they complete what they should do. I am suspecting if the app gets frozen. How to check which page user saw at last? I would like to save that data to firebase to analyze.
Simply I want to know where users are quitting the app.
There isn't any default method which you can use in this case. This is what you can do, in your every view controller's viewDidAppear save that screen's name in UserDefaults overwriting the previous value. Now on every app launch, check if the app was crashed last time it was opened, if it was crashed then get the screen name from UserDefaults and save it to firebase.
If you don't know how to detect an app crash, use this link.
https://stackoverflow.com/a/37220742/1811810
Apple offer that NSSetUncaughtExceptionHandler to listen whether the app is crash,
so ,you can when app launch use this listener to save the crash log,
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
//get crash report
[self installUncaughtExceptionHandler];
}
and the implementation is:
- (void)installUncaughtExceptionHandler {
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}
void UncaughtExceptionHandler(NSException *exception) {
NSArray *arr = [exception callStackSymbols];
NSString *reason = [exception reason];
NSString *name = [exception name];
NSString *currentVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleVersion"];
NSString *urlStr = [NSString stringWithFormat:#"mailto://developer#googlemail.com?subject=CrashReport&body=YourSuggest!<br><br><br>""errorInfo(%#):<br>%#<br>-----------------------<br>%#<br>---------------------<br>%#",currentVersion,name,reason,[arr componentsJoinedByString:#"<br>"]];
NSURL *url = [NSURL URLWithString:[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
[[UIApplication sharedApplication]openURL:url];
}
And suggest that you can send email to tell developer,if the customer allow that.
I think this error info can contain the crash page's info,hope this can help you.

iOS-WatchKit File Transfers Work Unreliably

I've built an app for iOS 9 and WatchOS 2. The iOS app will periodically transfer image files from the iPhone to the Watch. Sometimes, these are pushed from the app, sometimes the Watch requests (pulls) them. If pulled, I make the requests asynchronous, and use the exact same iOS code to transfer images in both cases.
About half the time (maybe 2/3), the file transfer works. The other times, it appears that nothing happens. This is the same whether I'm pushing or pulling images.
On the iOS side, I use code similar to this (session activated already):
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
if (session.reachable) {
NSData *imgData = UIImagePNGRepresentation(img);
NSURL *tempFile = [[session watchDirectoryURL] URLByAppendingPathComponent: #"camera.png"];
BOOL success = [imgData writeToFile: [tempFile path] atomically: NO];
if (success) {
NSLog(#"transferFile:metadata:");
[session transferFile: tempFile metadata: nil];
} else {
NSLog(#"will not call transferFile:metadata:");
}
} else {
NSLog(#"Camera watch client not reachable.");
}
}
On the watch extension side, I have a singleton that activates the watch session and receives the file:
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file {
// pass the data file to the data listener (if any)
[self.dataListener session: session didReceiveFile: file];
}
My "data listener" converts the file to a UIImage and displays it on the UI thread. However, that's probably irrelevant, as the unsuccessful operations never get that far.
During unsuccessful transfers, session:didReceiveFile: is never called. If I inspect the iOS app's log, however, I see these messages only during the operations that fail:
Dec 26 15:10:47 hostname companionappd[74893]: (Note ) WatchKit:
application (com.mycompany.MyApp.watchkitapp), install status: 2,
message: application install success
Dec 26 15:10:47 hostname
companionappd[74893]: (Note ) WatchKit: Purging
com.mycompany.MyApp.watchkitapp from installation queue, 0 apps
remaining
What is happening here? It looks like the app is trying to reinstall the Watch app (?). When this is happening, I do not see the watch app crash/close and restart. It simply does nothing. No file received.
On the iOS side, I scale down the image to about 136x170 px, so the PNG files shouldn't be too big.
Any ideas what's going wrong?
Update:
I have posted a complete, minimal project that demonstrates the problem on Github here
I am now under the impression that this is a bug in the simulators. It seems to work more reliably on the Apple Watch hardware. Not sure if it's 100% reliable, though.
Apple bug report filed (#24023088). Will update status if there is any, and leave unsolved for any potential answers that may provide workarounds.
For me, not a single transfer was working anymore. Polling transfer.progress showed isTransferring == true, but I never got beyond 0 completed units.
I ended up:
Deleting apps on watch and iPhone
Rebooting both
Reinstalling
And it works.
This is how I managed to transfer files from phone to watch:
In order for this to work, the file must be locate in appGroupFolder, and "App Groups" must be enabled from Capabilities tab, for phone and watch.
In order to get appGroup folder use following line of code:
NSURL * myFileLocationFolder = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier: #"myGroupID"]; //something like group.bundle.projName
Once you got that use this to send message and handle response from watch:
[session sendMessage:#{#"file":myFileURL.absoluteString} replyHandler:^(NSDictionary<NSString *,id> * _Nonnull replyMessage) {
//got reply
} errorHandler:^(NSError * _Nonnull error) {
//got Error
}];
Even though WCSession *session = [WCSession defaultSession]; I have noticed that sometimes session is deallocated, so you might consider using [WCSession defaultSession]; instead.
To catch this on the phone use:
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler{
//message[#"file"] - addres to my file
//do stuff with it here
replyHandler(#{#"myResponse":#"responseData"}); //this call triggers replyHandler block on the watch
}
Now a if you didn't forget to implement WCSessionDelegate and use
if ([WCSession isSupported]) {
_session = [WCSession defaultSession];
_session.delegate = self;
[_session activateSession];
}
//here session is #property (strong, nonatomic) WCSession * session;
It all should work.
Made a broader answer, hopefully will reach out to more people.

WatchKit Extension is not seeing data saved in NSUserDefaults with App Group

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
}

Pass message to Apple Watch using MMWormhole

I'm developing an Apple Watch App, and I need to notify the watch when certain changes occur in the parent application. I'm using the MMWormhole library found on GitHub, but I'm having trouble passing messages from the phone to the watch. Here is my code, do you have any ideas on why this is happening?
My main viewController code looks like this
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:#"com.mycompany.myapp"
optionalDirectory:#"wormhole"];
NSString *myString = [[NSString alloc] initWithFormat:#"Test String"];
[self.wormhole passMessageObject:#{#"string" : myString}
identifier:#"messageIdentifier"];
My InterfaceController from my WatchkitExtension looks like this:
InterfaceController.m
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
// Initialize the wormhole
self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:#"com.mycompany.myapp"
optionalDirectory:#"wormhole"];
// Obtain an initial value for the selection message from the wormhole
id messageObject = [self.wormhole messageWithIdentifier:#"messageIdentifier"];
NSString *string = [messageObject valueForKey:#"string"];
if (string != nil) {
NSLog(string);
[myLabel setText:string];
}
// Listen for changes to the selection message. The selection message contains a string value
// identified by the selectionString key. Note that the type of the key is included in the
// name of the key.
[self.wormhole listenForMessageWithIdentifier:#"messageIdentifier" listener:^(id messageObject) {
NSString *string = [messageObject valueForKey:#"string"];
if (string != nil) {
[self.myLabel setText:string];
}
}];
}
Thank you!
Is "com.mycompany.myapp" the real value you use in the app? Because group identifiers have to start with group..
If you use a wrong group identifier everything fails because the containerURLForSecurityApplicationGroupIdentifier call inside MMWormhole returns nil. Unfortunately the developers of MMWormhole didn't do any checks or asserts to make sure that the shared group identifier is correct.
So I would recommend to stop concentrating on MMWormhole for a minute. Instead add this code early in your code (e.g. applicationDidFinishLaunching) to verify that your container identifier is correct:
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSURL *appGroupContainer = [fileManager containerURLForSecurityApplicationGroupIdentifier:#"group.com.mycompany.myapp"];
if (!appGroupContainer) {
NSLog(#"group identifier incorrect, or app groups not setup correctly");
}
This will tell you if your app group setup is incorrect.
I'm not sure how far you are into setting up app groups, but you have to use the group identifier you used in the App Groups capabilities section of your project.

Resources