watchOS 2.0 - Can't cancel WCSessionFileTransfer - ios

Our app lets a user select records on iPhone that they want to be displayed in the watch app.
It works like this:
The user taps "Add to watch" on a record from their iPhone
A new version of the watch database is generated and sent to the watch
The watch app receives and saves the file and updates its interface
A new database file is sent to the watch and processed for each change. This is fine if the watch is awake since it will give the user live updates, but if the watch is asleep while the user makes 7 changes, it means the watch is accepting and processing 7 new files as soon as it wakes up.
We really only care about the most recent version of the watch database, so I'm trying to cancel all old outstanding file transfers.
Code:
On iPhone, each time a record is added/removed from watch database, we attempt (unsuccessfully) to cancel pending file transfers and then queue the latest database file:
// create watch database and store it at self.urlToDatabase
[self generateNewWatchDatabase];
if ([WCSession isSupported])
{
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
// this is the problem - cancel doesn't seem to do anything
for (WCSessionFileTransfer *fileTransfer in session.outstandingFileTransfers)
[fileTransfer cancel];
[session transferFile:self.urlToDatabase metadata:nil];
}
In the above code, calling [fileTransfer cancel] successfully removes the WCSessionFileTransfer object from session.outstandingFileTransfers, but didReceiveFile is still being called multiple times below.
Accepting the file on the watch:
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file
{
// this method gets called once for every time -transferFile:metadata: is called above,
// even after cancelling outstanding file transfers
[self replaceDatabaseWithFile:file];
[self refreshItemsTable];
}
How do we cancel outstanding file transfers?
Edit
As per #ccjensen's recommendation, I tried the following in the method that fires when the user adds/removes a record to/from the watch:
// store a reference to the file transfer and immediately cancel it
WCSessionFileTransfer *transfer = [session transferFile:self.urlToDatabase metadata:nil];
[transfer cancel];
This still results in the file being sent to the watch, instead of cancelling it as one would expect.
I also tried the following:
Kill watch app (by holding the side button until 'Power Off' appears, and then holding it again)
Add/remove records from iPhone
Relaunch watch app
Even in this scenario the watch receives all 'cancelled' file transfers.

The documentation for the cancel method says:
Use this method to cancel a file transfer before it completes. If the file has already been transferred, calling this method has no effect.
So it sounds like the cancels are "best effort" and might not end up being able to cancel them in all cases, especially if the file has already been transferred.
Are you seeing it never work, even if you call cancel immediately (try testing without the watch app running as that seems to expedite the transfers)?

turned out the reason this worked was that the file url did not match the transfer's url I was checking 🙈
I recently found that keeping hold of the WCSessionFileTransfer in my own array and canceling them proved more reliable than using [WCSession defaultSession].outstandingFileTransfers.
NSMutableArray<WCSessionFileTransfer*>* inProgressTransfers = [NSMutableArray array];
So each time you call TransfeFile: metaData:
WCSessionFileTransfer* transfer = [[WCSession defaultSession] transferFile:url metadata:metadata];
if (transfer)
{
[self.inProgressTransfers addObject:transfer];
}
then at an appropriate time
for (WCSessionFileTransfer* ourTransfer in self.inProgressTransfers)
{
[ourTransfer cancel];
[self.inProgressTransfers removeObject:ourTransfer];
}
For some reason, keeping hold of the transfers ourself makes calling cancel work much more reliably.. hope that helps someone

Related

WatchKit extension crash: "Program ended with exit code: 0"

For people wanting to reply quickly without reading the post: I am not hitting any memory limits. Read the whole post for details.
My WatchKit extension cannot properly function without the user first being "onboarded" through the phone app. Onboarding is where the user must accept the permissions that we require, so it's very crucial.
On my WatchKit extension, I wanted to display a simple warning for users who had not finished onboarding within our phone app yet.
As such, I thought I'd get the status of onboarding from the phone in two ways:
When the user opens the app/the app is activated (I use the willActivate method to detect this)
When the app finishes onboarding it sends a message to the watch of its completion (if the extension is reachable, of course)
Both of these combined would ensure that the status of onboarding is always kept in sync with the watch.
I wrote the first possibility in, utilizing reply handlers to exchange the information. It worked just fine, without any troubles. The warning telling the user to complete disappears, the extension does not crash, and all is well.
I then wrote in the second possibility, of the extension being reachable when the user finishes onboarding (with the phone then directly sending the companion the new status of onboarding). My extension crashes when it receives this message, and I am stuck with this odd error.
Program ended with exit code: 0
My extension does not even get a chance to handle the new onboarding status, the extension just quits and the above error is given to me.
I am not hitting any sort of memory limit. I have read the technical Q&A which describes what a memory usage limit error looks like, and I don't receive any sort of output like that whatsoever. As well, before the extension should receive the message, this is what my memory consumption looks like.
I have monitored the memory consumption of the extension right after finishing onboarding, and I see not a single spike indicating that I've gone over any kind of threshold.
I have tried going line by line over the code which manages the onboarding error, and I cannot find a single reason that it would crash with this error. Especially since the reply handler method of fetching the onboarding status works so reliably.
Here is the code of how I'm sending the message to the watch.
- (void)sendOnboardingStatusToWatch {
if(self.connected){
[self.session sendMessage:#{
LMAppleWatchCommunicationKey: LMAppleWatchCommunicationKeyOnboardingComplete,
LMAppleWatchCommunicationKeyOnboardingComplete: #(LMMusicPlayer.onboardingComplete)
}
replyHandler:nil
errorHandler:^(NSError * _Nonnull error) {
NSLog(#"Error sending onboarding status: %#", error);
}];
}
}
(All LMAppleWatchCommunicationKeys are simply #define'd keys with exactly their key as the string value. ie. #define LMAppleWatchCommunicationKey #"LMAppleWatchCommunicationKey")
Even though it's never called by the extension, here is the exact receiving code of the extension which handles the incoming data, if it helps.
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message {
NSString *key = [message objectForKey:LMAppleWatchCommunicationKey];
if([key isEqualToString:LMAppleWatchCommunicationKeyOnboardingComplete]){
BOOL newOnboardingStatus = [message objectForKey:LMAppleWatchCommunicationKeyOnboardingComplete];
[[NSUserDefaults standardUserDefaults] setBool:newOnboardingStatus
forKey:LMAppleWatchCommunicationKeyOnboardingComplete];
dispatch_async(dispatch_get_main_queue(), ^{
for(id<LMWCompanionBridgeDelegate> delegate in self.delegates){
if([delegate respondsToSelector:#selector(onboardingCompleteStatusChanged:)]){
[delegate onboardingCompleteStatusChanged:newOnboardingStatus];
}
}
});
}
}
Before including this onboarding-related code, my WatchKit extension was tested by over 100 people, without any troubles. I am using the exact same custom error dialogue that I was using before, just with a different string. I cannot for the life of me figure out what is causing this crash, and the ambiguity of it has given me very little to work with.
Any help would be greatly appreciated. Thank you very much for taking your time to read my post.
Edit: I just tried creating a symbolic breakpoint for exit(), which is never hit. If I call exit() myself, it calls the breakpoint, so I know the breakpoint itself is working.

How to specify what should and shouldn't be tracked as a session (not event) when we add Google Analytics to an iOS app

If we instantiate session (not event, action or screen) tracking with Google Analytics for an iOS or Watch app, is there any way to specify what should and shouldn't be tracked as a session for example not to call the start lifecycle tracking on each launch -- e.g. I don't want background tasks or resuming the watch app or activating the glance to count as sessions. As a developer, do I have control over it to filer the session tracking data before sending to Google or all session tracking data are automatically dispatched to Google? currently I call this function in AppDelegate didFinishLaunchingWithOptions
(void) initializeGoogleAnalytics: (NSString * ) containerFilename {
if ([self googleTagManager] == nil) {
TAGManager * tagManager = [TAGManager instance];
[tagManager setLogger: [TWNGTMLogger logger]];
[tagManager setDispatchInterval: 20.0];
[tagManager setDispatchInterval: 1.0];
[self setGoogleTagManager: tagManager];
//Open GTM tag container (async)
[TAGContainerOpener openContainerWithId: containerFilename tagManager: [self googleTagManager] openType: kTAGOpenTypePreferFresh timeout: nil notifier: self];
DMLogInfo(DebugLogTypeTracking, # "Google Analytics initialized, container: %#, version: %#", containerFilename, kGAIVersion);
}
}
1) Is there a way to turn off or turn on Session Tracking in GTM code on iOS?
There's not a way to adjust session tracking itself using the GTM UI or the datalayer, other than preventing GA tags from firing (mentioned below).
2) Is there a way to AVOID session tracking if the app is launched in background?
The main way is to prevent the tag from firing when the app is opened in the background would be to set up a "blocking trigger" that looks for an event the indicates the app was launched in the background. Then, even if datalayer events were being fired telling the tag to fire, the blocking trigger would prevent the tag from firing, and therefore prevent a session from starting.

How to correctly implement transfer of settings from iOS app to watchOS2 complication

What I want to achieve is the following:
Complication(s) get updated in the background at intervals of 30
minutes
Complication(s) get updated whenever the watch app runs and
receives its own updated data
Complication(s) get updated whenever
the iOS app runs and the user changes a setting which affects the
watch data (such as changing location of weather observations, or
display units)
Items 1. and 2. seem to be straightforward, and nicely addressed here: What is the flow for updating complication data for Apple Watch?
However, for item 3, in the iOS app, I set up a WCSession instance and call transferCurrentComplicationUserInfo, sending the new settings as NSDictionary. In the watch extension, this invokes didReceiveUserInfo in WCSessionDelegate.
- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary<NSString *,id> *)userInfo {
// Code here to apply the new settings
// ....
// Invoke a NSUSRLSession-based web query to get new data
[self queryForNewDataWithCompletionHandler:^(NCUpdateResult result) {
if (result == NCUpdateResultNewData) {
// Have new data from web to display
CLKComplicationServer *server = [CLKComplicationServer sharedInstance];
for (CLKComplication *complication in server.activeComplications) {
[server reloadTimelineForComplication:complication];
}
}
// Set date for next complication update to 30 mins from now
// ...
}];
}
The problem I am having is that watchOS is calling requestedUpdateDidBegin in a separate thread, shortly after it invoked didReceiveUserInfo and this starts executing BEFORE I have a chance to obtain updated data using the new settings in the newly received UserInfo dictionary from the app.
Consequently, the complications get updated twice in short succession - once by the WatchOS having called requestedUpdateDidBegin, which simply re-updates the complication with existing (stale) data, before I very soon after receive new data from the web and then have to update them again in my own code.
This seems unnecessary and a waste of resources, not to mention the limited budget of updates that Apple allows (supposedly 2 per hour).
Am I doing anything wrong here? How can I prevent watchOS2 from calling requestedUpdateDidBegin before I have had a chance to acquire new data from the web?
The purpose of transferCurrentComplicationUserInfo is to immediately pass current complication data to the extension. In your code, you are passing settings, however you are not including any weather data.
The issue you're seeing stems from trying to asynchronously fetch new data within the extension (which is returning before the data is available).
To handle this, you should fetch the current weather data on the phone based on the new settings, then pass (the new settings along with) the weather data in the current complication user info.
- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary<NSString *,id> *)userInfo {
// Code here to apply the new settings for future updates
// ....
// Code here to update cache/backing store with current weather data just passed to us
// ....
CLKComplicationServer *server = [CLKComplicationServer sharedInstance];
for (CLKComplication *complication in server.activeComplications) {
[server reloadTimelineForComplication:complication];
}
}
This way, the complication server can immediately update the timeline using the current complication data you just transferred to the watch.
No stale data, no unnecessary second update.

NSNotificationCenter callback while app in background

One question and one issue:
I have the following code:
- (void) registerForLocalCalendarChanges
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(localCalendarStoreChanged) name:EKEventStoreChangedNotification object:store ];
}
- (void) localCalendarStoreChanged
{
// This gets call when an event in store changes
// you have to go through the calendar to look for changes
[self getCalendarEvents];
}
These methods are in a class/object called CalendarEventReporter which contains the method getCalendarEvents (in the callback).
Two things:
1) If the app is in the background the callback does not run. Is there a way to make it do that?
2) When I bring the app back into the foreground (after having changed the calendar on the device) the app crashes without any error message in the debug window or on the device. My guess is that the CalendarEventReporter object that contains the callback is being garbage-collected. Is that possible? Any other thoughts on what might be causing the crash? Or how to see any error messages?
1) In order for the app to run in the background you should be using one of the modes mentioned in the "Background Execution and Multitasking section here:
uses location services
records or plays audio
provides VOIP
services
background refresh
connection to external devices
like through BLE
If you are not using any of the above, it is not possible to get asynchronous events in the background.
2) In order to see the crash logs/call stack place an exception breakpoint or look into the "Device Logs" section here: Window->Organizer->Devices->"Device Name" on left->Device Logs on Xcode.
To answer your first question, take a look at https://developer.apple.com/library/ios/documentation/iphone/conceptual/iphoneosprogrammingguide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html
What I did to get code running in the background is to do something like
In the .h file
UIBackgroundTaskIdentifier backgroundUploadTask;
In the .m file
-(void) functionYouWantToRunInTheBackground
{
self.backgroundUploadTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundUpdateTask];
}];
//code to do something
}
-(void) endBackgroundUpdateTask
{
[[UIApplication sharedApplication] endBackgroundTask: self.backgroundUploadTask];
self.backgroundUploadTask = UIBackgroundTaskInvalid;
}
The code above I pretty much learned from objective c - Proper use of beginBackgroundTaskWithExpirationHandler
As for your second question, you should set a breakpoint where code is supposed to run when you bring the app back to the foreground. No one can figure out why an app crashes if not given enough code or information.
The solution to the second part of the question was to raise the scope of the object containing the callback code. I raised it to the level of the containing ViewController. This seems to work. I still can't figure out how to raise the Notification (i.e. execute the call back) if the notification comes while the app is in the background/suspended. This prevented the object containing the callback from being cleaned up.

NSurlSession - downloading many files

I have a requirement to download a number of files (around 500). I have an array containing all the urls of these files, I wanted to use NSURLSession so that i can support background downloading too.
I cant think of correct way to achieve this. If i am initiating next file download after one is completed then background downloading will not work.
shall I creating multiple downloading tasks and initiate?
Please suggest me how to achieve this.
Edit:
First, sorry for late response and here is a solution for your problem. Begin with downloading Apple's Simple Background Transfer sample. Then you will see the URLSessionDidFinishEventsForBackgroundURLSession method in view controller. You can modify this method for calling another download task like below sample and I think this is what you want to do.
There is also a comment over this method like this the session delegate will receive this message to indicate that all messages previously enqueued for this session have been delivered. So creating a queue for your requests could be better solution then this.
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
APLAppDelegate *appDelegate = (APLAppDelegate *)[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
appDelegate.backgroundSessionCompletionHandler = nil;
completionHandler();
[self start:nil];
}
NSLog(#"All tasks are finished");
}
I was having problems with this. My app had to update itself and download news videos in the background using BACKGROUND FETCH to get json list of files then firing off n number of webservice calls to download these files using BACKGROUND TRANSFER
[NSURLSessionConfiguration backgroundSessionConfiguration:
For each file I was creating one NSSession and one NSURLSessionDownloadTask.
file 1 - NSSession1 > NSURLSessionDownloadTask1
file 2 - NSSession2 > NSURLSessionDownloadTask2
file 3 - NSSession3 > NSURLSessionDownloadTask3
This woke fine when the app was in the foreground.
But I had problems when the app was in background and woken by BACKGROUND FETCH
One file would download and then it would halt.
It was like only the first NSSession1 was executed.
It may have been that iOS was waiting till device was idle again to run next session but this was too slow
I got it working by having one NSSession and attaching all NSURLSessionDownloadTask3
NSURLSession * backgroundSession_ =
for(url to call){
create NSURLSessionDownloadTask1 (set its session:backgroundSession_)
create NSURLSessionDownloadTask2 (set its session:backgroundSession_)
create NSURLSessionDownloadTask3 (set its session:backgroundSession_)
}
Be careful when doing this
call NSSession finishTasksAndInvalidate not invalidateAndCancel
//[session invalidateAndCancel];
[session finishTasksAndInvalidate];
invalidateAndCancel will stop the session and not finish the other download tasks

Resources