WCSession's transferUserInfo no longer reliably working in watchOS 2.2 with iOS 9.3
Asked Answered
F

2

9

I have an existing iOS 9.2 and watchOS 2.1 app that uses sendMessage and transferUserInfo to send data from the iPhone to the Apple Watch. If sendMessage fails, I am using transferUserInfo to queue the data for later delivery:

// *** In the iOS app ***
self.session.sendMessage(message, replyHandler: nil) { (error) -> Void in
    // If the message failed to send, queue it up for future transfer
    self.session.transferUserInfo(message)
}

// *** In the watchOS app ***
func session(session: WCSession, didReceiveMessage message: [String : AnyObject]) {
    // Handle message here
}

func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
    // Handle message here
}

Without changing any code and running the app on iOS 9.3 with watchOS 2.2 on a real device (simulator does not have the same problem), sendMessage delivers data to the Apple Watch as long as the watch is within range and the screen is on. This is as expected and how it previously worked. However, if the screen is off and sendMessage fails, transferUserInfo no longer delivers data to the Apple Watch when the screen turns back on.

In an attempt to find where this was erroring out, I added the following WCSessionDelegate method to see if the iOS app was failing to send the data:

func session(session: WCSession, didFinishUserInfoTransfer userInfoTransfer: WCSessionUserInfoTransfer, error: NSError?) {
    // Called when self.session.transferUserInfo completes
}

This method does get invoked after transferUserInfo is called, but there is no error returned and the iOS app seems to indicate that the transfer occurred successfully.

At first I thought that perhaps the time it takes to transfer the data has been increased, but after leaving the device alone for a day the data still had not transferred. I am now somewhat suspicious it has something to do with the new multi-watch API, and perhaps the iOS app needs to know a specific watch to send it to, though I only have ever had a single watch paired. Does anyone have any ideas on what might have changed and how to correctly use transferUserInfo?

Factotum answered 29/3, 2016 at 13:58 Comment(5)
We are having a similar issue, sending userInfo from watch to phone, and sometimes delegate not getting called at all. Prior 9.3 and 2.2 this was working.Monocot
have a look to this post forums.developer.apple.com/thread/43596Monocot
I am the second user comment there :) That post makes me even more suspicious of the new multi-watch API's.Factotum
I was just playing with transferCurrentComplicationUserInfo and was noticing the same thing. This definitely looks like a new bug Apple introduced.Factotum
Fro me things break after I delete the app on the phone then reinstall it. After that, transferCurrentComplicationUserInfo: never seems to successfully transfer a userInfo payload to the watch. It's not until I reboot the watch do things start working again.Aspergillus
F
3

I think I have this working now. First, I had to add the new WCSessionDelegate methods to my iOS app:

@available(iOS 9.3, *)
func session(session: WCSession, activationDidCompleteWithState activationState: WCSessionActivationState, error: NSError?) {
    if activationState == WCSessionActivationState.Activated {
        NSLog("Activated")
    }

    if activationState == WCSessionActivationState.Inactive {
        NSLog("Inactive")
    }

    if activationState == WCSessionActivationState.NotActivated {
        NSLog("NotActivated")
    }
}

func sessionDidBecomeInactive(session: WCSession) {
    NSLog("sessionDidBecomeInactive")
}

func sessionDidDeactivate(session: WCSession) {
    NSLog("sessionDidDeactivate")

    // Begin the activation process for the new Apple Watch.
    self.session.activateSession()
}

And similarly to my watchOS app:

@available(watchOSApplicationExtension 2.2, *)
func session(session: WCSession, activationDidCompleteWithState activationState: WCSessionActivationState, error: NSError?) {
    if activationState == WCSessionActivationState.Activated {
        NSLog("Activated")
    }

    if activationState == WCSessionActivationState.Inactive {
        NSLog("Inactive")
    }

    if activationState == WCSessionActivationState.NotActivated {
        NSLog("NotActivated")
    }
}

But transferUserInfo was still not working for me, specifically when the Apple Watch's screen was off. Below is how I was sending information between an iPhone and Apple Watch in iOS 9.2/watchOS 2.1:

func tryWatchSendMessage(message: [String : AnyObject]) {
    if self.session != nil && self.session.paired && self.session.watchAppInstalled {
        self.session.sendMessage(message, replyHandler: nil) { (error) -> Void in
            // If the message failed to send, queue it up for future transfer
            self.session.transferUserInfo(message)
        }
    }
}

I had assumed that sending a message from the iPhone to the Apple Watch when the watch's screen was off was causing transferUserInfo to fail because it was in the error handler of sendMessage. sendMessage also worked as expected when the screen was on. However, it looks like sendMessage's error reply handler does not always get called if your watch's screen is off, even though the request fails. This is different behavior from the previous OS versions. This also seems to have caused a cascade effect where subsequent messages also failed even though conditions were appropriate. This is what made me believe that transferUserInfo was to blame.

I found that in order for my messages to go through reliably, I needed to check for both reachable and activationState. Since I also wanted to continue supporting earlier iOS and watchOS versions, my tryWatchSendMessage method became the following:

func tryWatchSendMessage(message: [String : AnyObject]) {
    if #available(iOS 9.3, *) {
        if self.session != nil && self.session.paired && self.session.watchAppInstalled && self.session.activationState == .Activated {
            if self.session.reachable == true {
                self.session.sendMessage(message, replyHandler: nil) { (error) -> Void in
                    // If the message failed to send, queue it up for future transfer
                    self.session.transferUserInfo(message)
                }
            } else {
                self.session.transferUserInfo(message)
            }
        }
    } else {
        // Fallback on earlier versions
        if self.session != nil && self.session.paired && self.session.watchAppInstalled {
            if self.session.reachable == true {
                self.session.sendMessage(message, replyHandler: nil) { (error) -> Void in
                    // If the message failed to send, queue it up for future transfer
                    self.session.transferUserInfo(message)
                }
            } else {
                self.session.transferUserInfo(message)
            }
        }
    }
}

Making these changes seem to have resolved the issues I was seeing. I am interested to see if these help resolve anyone else's issues, or if there are still related issues to transferUserInfo not working.

Factotum answered 30/3, 2016 at 20:8 Comment(8)
Good find... Although I'm now a little confused by your original question. It seems you didn't change the logic related to transferUserInfo, other than to protect sendMessage properly on 9.3?Incorporator
I wonder if there is actually an issue with threading. My understanding is that WCSession is a singleton. In this case, the reply and error handling will occur on a background thread. If you issue sendMessage from within those blocks it may be possible they are executed in parallel with the main thread. Have you tried queuing your sendMessage to the main thread instead of running it in background like this?Incorporator
tryWatchSendMessage is always called on the main thread for me, so sendMessage is already invoked on the main thread. The errorHandler may not be, but it is my understanding that it is safe to call transferUserInfo from outside the main thread.Factotum
You should review the sendMessage documentation. Block handlers are run on a background thread (at least according to Apple), this would include your replyHandler code, right?Incorporator
My reply handlers are nil. The problem was the error handler was not even getting called, so I couldn't even call transferUserInfo on a background thread.Factotum
You've resolved that it isn't related to transferUserInfo being sent but not delivered. (May want to update the dev forum thread to clear up that report!) While your new code avoids triggering a reachable error in the first place, the error handler should catch that: "When sending messages, the most common error is that the paired device was not reachable, but other errors may occur too." Curious. Perhaps you could ask a new question to specifically determine why your iOS error handler block doesn't get called?Phobos
Agreed, the real bug I was seeing is that the error handler is not reliably getting called in sendMessage.Factotum
It works from iPhone Simulator to watch simulator but not working from watch simulator to iPhone simulator. Can you help me out ?Leclerc
P
0

I was having very similar problems with getting communication to work since the 2.2 update last week - but for me I couldn't get the app context or files to transfer over.

Like everyone else, it worked in the simulator, but not on the device.

I noticed today that I was attempting to send data from a background thread - I wrapped all my calls in a dispatch_async(dispatch_get_main_queue()) and suddenly everything is now working.

Peptize answered 4/4, 2016 at 20:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.