WCSession transferUserInfo only works in Foreground - ios

I am using WCSession's tranferUserInfo to send data between the watch and the iOS app for info that needs to be handled when either product is in the background. This works 100% of the time on the simulator but never with actual devices.
By using breakpoints I have discovered that func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) is never called in the background but is immediately called when the app is brought to the foreground. Clearly session.transferUserInfo(data) is being called but not received in the background state. Again running the exact same code but on the simulator works perfectly.
I am running iOS 9.3.2 and Watch OS 2.2.1. Clearly this function was meant to handle communications in the background state and thus I believe the simulator is working as intended. I tried wrapping both the sender and receiver in a dispatch_async(dispatch_get_main_queue(), { block, but to no avail.
What am I missing about transferUerInfo and its seeming inability to work properly with background states?
FYI - breakpoint set at the beginning of didRecieveUserInfo is never hit until the app is brought into the foreground.
func transferInfo(data:[String: AnyObject])
{
dispatch_async(dispatch_get_main_queue(), {
if #available(watchOSApplicationExtension 2.2, *)
{
if #available(iOS 9.3, *)
{
if self.session.activationState == .Activated
{
self.session.transferUserInfo(data)
}
else
{
NSNotificationCenter.defaultCenter().postNotificationName("alertError", object: self, userInfo: ["error":"Failed to transfer"])
}
}
else
{
self.session.transferUserInfo(data)
}
}
else
{
self.session.transferUserInfo(data)
}
})
}
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject])
{
dispatch_async(dispatch_get_main_queue(), {
for delegate in self.watchCommsProtocols
{
delegate.watchCommsDidUpdateInfo!(userInfo)
}
})
}

I just watched the Watch Connectivity session from WWDC 2015. It seems that transferUserInfo cannot be received by iOS until the app is in the foreground. That is of course what I am seeing with actual devices. The issue here then, and what has thrown me off, is that the simulator as of this writing DOES receive these messages when in the background. This is not the correct behavior and should therefore be considered a bug in the functioning of the simulator.
For my purposes I should be able to use sendMessagefrom the watch to iOS when iOS is in the background. However, the same is not true in reverse. To use sendMessage from iOS to the watch, the watch will have to be in the foreground.

Both sides can send while the sending app is in background.
This means
if you are sending while there is no connection,
then your sending app goes into background or stops,
then there is a connection
-> the sending OS will send.
An app in watchOS 2 can't do anything in background. So it can't receive.
On iOS, an app can't bring itself to the foreground while in background. So dispatch_async(dispatch_get_main_queue() doesn't make sense here.

Related

Data is not being transferred from iphone to iWatch (AppleWatch) on real devices

I have an iOS app and I programmed a extension for it on appleWatch.
I'm sending data (NSDictionary) to the appleWatch extension using transferUserInfo method. Everything works in the simulator but when I'm trying to run the application on real devices, it seems that the iWatch is not receiving anything though the iPhone is sending the data (I found this cause I debugged the sender side).
I have configured the WCSession on both sides. I have conformed the WCSessionDelegate in the class where I'm supposed to receive the data.
I'm using session:didReceiveUserInfo: method to receive the data in ExtensionDelegate but still like I said everything works fine in the simulator but nothing is being transferred on real devices.
Does anyone has a clue to what the problem is?
here's the code:
in the sender side:
inside my class MensaViewController.m
- (void)viewDidLoad
if ([WCSession isSupported]) {
session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
NSLog(#"A dish is being sent");
[session transferUserInfo:dishDictionary];
}
dishDictionary is declared inside viewDidLoad method and it contains data.
on the receiver side (Watch Extension)
I configure the WCSession and receive data in ExtensionDelegate.m like this:
- (void)applicationDidBecomeActive {
if ([WCSession isSupported]) {
session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
NSLog(#"Session activated in iWatch");
}
}
and I have this method to receive the data:
- (void)session:session didReceiveUserInfo:(NSDictionary<NSString *,id> *)userInfo{
NSLog(#"Received data from the iPhone");
NSArray<NSString*> *dictionaryKeys = userInfo.allKeys;
for(int i = 0; i < dictionaryKeys.count; i++){
Boolean equalsMensaString = [dictionaryKeys[i] isEqualToString:#"beilagen"];
if(equalsMensaString)
[self handleTransferredDish:userInfo];
Boolean equalsNewsString = [dictionaryKeys[i] isEqualToString:#"article"];
if(equalsNewsString)
[self handleTransferredNews:userInfo];
Boolean equalsEventString = [dictionaryKeys[i] isEqualToString:#"description"];
if(equalsEventString)
[self handleTransferredEvents:userInfo];
}
}
If you take a look at sources function description, you can see the description of the method you are using for transferring data:
public func transferUserInfo(userInfo: [String : AnyObject]) -> WCSessionUserInfoTransfer
The system will enqueue the user info dictionary and transfer it to
the counterpart app at an opportune time. The transfer of user info
will continue after the sending app has exited. The counterpart app
will receive a delegate callback on next launch if the file has
successfully arrived. The userInfo dictionary can only accept the
property list types.
So..There's no guarantee that the system will send this userInfo while you are actually using the App.
Use the following method instead:
public func sendMessage(message: [String : AnyObject], replyHandler: (([String : AnyObject]) -> Void)?, errorHandler: ((NSError) -> Void)?)
Clients can use this method to send messages to the counterpart app. Clients wishing to receive a reply to a particular
message should pass in a replyHandler block. If the message cannot be
sent or if the reply could not be received, the errorHandler block
will be invoked with an error. If both a replyHandler and an
errorHandler are specified, then exactly one of them will be invoked.
Messages can only be sent while the sending app is running. If the
sending app exits before the message is dispatched the send will fail.
If the counterpart app is not running the counterpart app will be
launched upon receiving the message (iOS counterpart app only). The
message dictionary can only accept the property list types.
and for receiving:
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void)
Notice That:
This send can fail if the counterpart app is not open or the Session is not paired and active. So, in the case you can not miss any data you should use updateApplicationContext or transferUserInfo (I actually prefer updateApplicationContext)
session.sendMessage(messageData, replyHandler: { (replyData) -> Void in
replyHandler?(replyData)
}, errorHandler: { (error) -> Void in
print("error: code:\(error.code) - \(error.localizedDescription)")
errorHandler?(error)
do {
try session.updateApplicationContext(messageData)
} catch {
print("There was an error trying to update watchkit app on the background")
}
})
And make sure you receive this cases with proper implementation of
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject])
I know I'm too late to answer my own question. We were using Apple Watch 2 and IPhone 5 or 6 (don't remember after 2 years!), what happened was magical as everything started to work as the batterie of the apple watch was really low (in 5-10% range). We didn't have any problem even after charging the apple watch. End!

Apple Watch Background Mode?

I am developing apple watch application. when i run the app it is working fine. Now my problem is when the app goes to background mode, the app on the apple watch app will closing automatically. I am writing small code in iPhone app:
func viewDidLoad() {
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
// In your WatchKit extension, the value of this property is true when the paired iPhone is reachable via Bluetooth.
// On iOS, the value is true when the paired Apple Watch is reachable via Bluetooth and the associated Watch app is running in the foreground.
// In all other cases, the value is false.
if session.reachable {
lblStatus.text = "Reachable"
}
else
{
lblStatus.text = "Not Reachable"
}
func sessionReachabilityDidChange(session: WCSession)
{
if session.reachable {
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.text = "Reachable"
})
}
else
{
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.text = "Not Reachable"
})
}
}
}
}
in WatchExtention Code is
func someFunc() {
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
if session.reachable {
ispatch_async(dispatch_get_main_queue(), {
self.lblStatus.setText("Reachable")
})
}
else
{
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.setText("Not Reachable")
})
}
func sessionReachabilityDidChange(session: WCSession)
{
if session.reachable {
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.setText("Reachable")
})
}
else
{
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.setText("Not Reachable")
})
}
}
}
}
Now when enter to background in apple Watch the iPhone app showing Not reachable why ?
It's the default behavior of the AppleWatch, mainly to spare with resources like battery.
session.reachable property is true only when Apple Watch is reachable via Bluetooth and the associated Watch app is running in the
foreground in all other cases, the value is false.
In your case the second option which caused the problem I suppose the bluetooth connection is working.
Anyway the question is what do you like to reach.
Actually the simple rule is that you couldn't wake up the Watch from the iPhone but you could wake up the iPhone app from the Watch.
Two ways with three options to reach the watch when it's counterpart is in the background: send a complication update or send a message (2 options) in the background which will be available for the Watch when it will awake again.
All of them are part of the WCSession Class.
The two options for sending messages are:
- updateApplicationContext:error:
You can use this method to transfer a dictionary of data to the counterpart Watch app.iPhone sends context data when the opportunity arises, means when the Watch app arises.The counterpart’s session on the Watch gets the data with the session:didReceiveUpdate: method or from the receivedApplicationContext property.
You may call this method when the watch is not currently reachable.
The other option is sending data in the background like
- transferUserInfo:
You can use this method when you want to send a dictionary of data to the Watch and ensure that it is delivered. Dictionaries sent using this method are queued on the other device and delivered in the order in which they were sent. After a transfer begins, the transfer operation continues even if the app is suspended.
BUT true for both methods that they can only be called while the session is active. Calling any of these methods for an inactive or deactivated session is a programmer error.
The complication solution is a little bit different but belongs to the same WCSession Class as the earliers.
-transferCurrentComplicationUserInfo:
This method is specifically designed for transferring complication user info to the watch with the aim to be shown on the watch face immediately.
Of course it's available only for iOS, and using of this method counts against your complication’s time budget, so it's availability is limited.
The complication user info is placed at the front of the queue, so the watch wakes up the extension in the background to receive the info, and then the transfer happens immediately.
All messages received by your watch app are delivered to the session delegate serially on a background thread, so you have to switch to the main queue in case you'd like to use or presenting them for UI.
The WWDC talk on WatchConnectivity discusses "reachability" and its nuances in quite a lot of detail, so you should definitely give it a watch.
TL;DR: reachable on the watch is for the most part only going to be true/YES when the watch app's UI is visible on the screen.

WatchOS2 WCSession sendMessage doesn't wake iPhone on background

This is being tested on both Simulator and real physical device iphone5s. I tried to use WCSession sendMessage to communicate from WatchOS2 extension to iPhone iOS9 code. It works well when iphone app is running either in the foreground and background mode.
But If I kill the iPhone app (not running app at all), then I always got errorHandler timeout. So Watch cannot communicate with iPhone anymore.
"Error Domain=WCErrorDomain Code=7012 "Message reply took too long."
UserInfo={NSLocalizedDescription=Message reply took too long.,
NSLocalizedFailureReason=Reply timeout occured.}".
I think it supposed to wake iPhone app in the background.
Any idea what to work around this problem or fix it? Thank you!
It is important that you activate the WCSession in your AppDelegate didFinishLaunchingWithOptions method. Also you have to set the WCSessionDelegate there. If you do it somewhere else, the code might not be executed when the system starts the killed app in the background.
Also, you are supposed to send the reply via the replyHandler. If you try to send someway else, the system waits for a reply that never comes. Hence the timeout error.
Here is an example that wakes up the app if it is killed:
In the WatchExtension:
Setup the session. Typically in your ExtensionDelegate:
func applicationDidFinishLaunching() {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
And then send the message when you need something from the app:
if WCSession.defaultSession().reachable {
let messageDict = ["message": "hello iPhone!"]
WCSession.defaultSession().sendMessage(messageDict, replyHandler: { (replyDict) -> Void in
print(replyDict)
}, errorHandler: { (error) -> Void in
print(error)
}
}
In the iPhone App:
Same session setup, but this time also set the delegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
...
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
And then implement the delegate method to send the reply to the watch:
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
replyHandler(["message": "Hello Watch!"])
}
This works whenever there is a connection between the Watch and the iPhone. If the app is not running, the system starts it in the background.
I don't know if the system waits long enough until you received your data from iCloud, but this example definitely wakes up the app.
After hours of trying and hint from #jeron. I finally figured out the problem myself.
In my session:didReceiveMessage delegate method, I have two calls. 1.replyHandler call. 2. I have an async process running (RXPromise) in my case, It nested quite a few RXPromise callbacks to fetch various data from cloud service. I didn't pay attention to it, because it is supposed to call and return right away. But now that I commented out RXPromise block all together, it can wake up iOS app in the background every time.
Finally I figure out the trouble make is because after RXPromise call, it is not guaranty to be landed back to main thread anymore. And I believe session:didReceiveMessage has to be return on the main thread. I didn't see this mentioned anywhere on the Apple documentation.
Final solution:
- (void)session:(WCSession *)session
didReceiveMessage:(NSDictionary<NSString *, id> *)message
replyHandler:(void (^)(NSDictionary<NSString *, id> *_Nonnull))replyHandler {
replyHandler(#{ #"schedule" : #"OK" });
dispatch_async(dispatch_get_main_queue(), ^{
Nested RXPromise calls.....
});
}
Well, you can use transferUserInfo in order to queue the calls. Using sendMessage will result in errors when app is killed

How can i check from watchOS 2 if application on iPhone is opened or not and be able to send NSUserDefaults no matter the app status?

How can I check from watchOS 2 if application on iPhone is opened or not?
I want to send a message with NSUserDefaults from watch to iPhone via sendMessage (to be able to update interface on phone when message received) when both applications are running and I want to send NSUserDefaults even if only watchOS 2 app is running.
From what I read I found this:
/** The counterpart app must be reachable for a send message to succeed. */
#property (nonatomic, readonly, getter=isReachable) BOOL reachable;
It's always reachable from what I check.
Reachable means the apple watch and iPhone are connected via bluetooth or wifi. It doesn't necessarily mean the iPhone app is running. If reachable is true, when you try to sendMessage from the apple watch it will launch the iPhone app in the background. You need to assign the WKSession delegate as soon as possible because the delegates methods (sendMessage) will fire soon. I think what you are saying you want to do is call sendMessage if you can, and it not use the transferUserInfo method instead. To do this, first on your apple watch:
func applicationDidFinishLaunching() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
// NOTE: This should be your custom message dictionary
// You don't necessarily call the following code in
// applicationDidFinishLaunching, but it is here for
// the simplicity of the example. Call this when you want to send a message.
let message = [String:AnyObject]()
// To send your message.
// You could check reachable here, but it could change between reading the
// value and sending the message. Instead just try to send the data and if it
// fails queue it to be sent when the connection is re-established.
session.sendMessage(message, replyHandler: { (response) -> Void in
// iOS app got the message successfully
}, errorHandler: { (error) -> Void in
// iOS app failed to get message. Send it in the background
session.transferUserInfo(message)
})
}
Then, in your iOS app:
// Do this here so it is setup as early as possible so
// we don't miss any delegate method calls
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.watchKitSetup()
return true
}
func watchKitSetup() {
// Stop if watch connectivity is not supported (such as on iPad)
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
// Handle the message from the apple watch...
dispatch_async(dispatch_get_main_queue()) {
// Update UI on the main thread if necessary
}
}
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
// Handle the message from the apple watch...
dispatch_async(dispatch_get_main_queue()) {
// Update UI on the main thread if necessary
}
}
You probably want to use the application context of WatchConnectivity:
have a look at WCSession.updateApplicationContext( )
It sends the most important configuration info to the counterpart as soon as the counterpart is reachable, even if the counterpart is not reachable at the time of sending. If you call updateApplicationContext multiple times, only the latest is sent.
For much deeper info watch the WWDC 2015 session about WatchConnectivity: https://developer.apple.com/videos/wwdc/2015/?id=713
It describes more means to send data, but I think the application context fits best for you.
The session also details how to find out if the counterpart is reachable, but I think you don't need that for your use case.

How to wake up iPhone app from watchOS 2?

I have an app that has a very rich network layer and my apple watch app depends on all the models. Unfortunately the app is not modular enough to make this layer available in the watch app.
I solved this problem by using openParentApplication: to wake up the iPhone app, perform the request and give back the results.
In watchOS 2 this method is gone and I should use WatchConnectivity. The best way to use this would be by sending userInfo dictionaries.
But how can I wake up the iPhone app to handle my requests? To get notifications about new userInfos I have to use the WCSessionDelegate and for that I need a WCSession object. But when should I create that? And how to wake up the app?
I asked an Apple Engineer about this and got the following tip: The iOS-App should be started in a background-task. So the following worked for me pretty well:
UIApplication *application = [UIApplication sharedApplication];
__block UIBackgroundTaskIdentifier identifier = UIBackgroundTaskInvalid;
dispatch_block_t endBlock = ^ {
if (identifier != UIBackgroundTaskInvalid) {
[application endBackgroundTask:identifier];
}
identifier = UIBackgroundTaskInvalid;
};
identifier = [application beginBackgroundTaskWithExpirationHandler:endBlock];
Add this to your session:didReceiveMessage: or session:didReceiveMessageData: method to start a background task with a three minute timeout.
Swift version of Benjamin Herzog's suggestion below. Of note, while I did choose to push the work initiated by the Watch to a background task, as long as my app was in the background, the system woke it up just fine. Doing the work in a background task did not appear to be required, but is best practice.
override init() {
super.init()
if WCSession.isSupported() {
session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let taskID = self.beginBackgroundUpdateTask()
//Do work here...
self.endBackgroundUpdateTask(taskID)
})
var replyValues = Dictionary<String, AnyObject>()
let status = "\(NSDate()): iPhone message: App received and processed a message: \(message)."
print(status)
replyValues["status"] = status
replyHandler(replyValues)
}
func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
return UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({})
}
func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.sharedApplication().endBackgroundTask(taskID)
}
In the WatchKit extension you will want to use the WatchConnectivity WCSession sendMessage APIs. In the extension check that the iOS app is reachable, and then send the message:
let session = WCSession.defaultSession();
if session.reachable {
let message = ["foo":"bar"]
session.sendMessage(message, replyHandler: nil, errorHandler: { (error) -> Void in
print("send failed with error \(error)")
})
}
This message will cause the system to wake the iOS app in the background, so make sure to set up the WCSession in a piece of the iOS app code that gets called when running in the background (as an example: you don't want to put it in a UIViewController's subclass's viewDidLoad) so that the message is received. Since you will be requesting some information you might want to take advantage of the reply block.
So this is how you accomplish launching the iOS app in the background, though the WatchConnectivity WWDC session recommended trying to use the "background" transfer methods if possible. If your watch app is read-only then you should be able to queue up any changes on the iOS device using the background transfers and then they will be delivered to the watch app next time it runs.

Resources