Search code examples
iosobjective-cwatchkitwatchos-2

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


Solution

  • 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.