Apple Watch Background Mode? - ios

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.

Related

Use Bloothtooth LE while app is in background

I am building an iOS app with Xamarin, with this BLE plugin:
https://github.com/aritchie/bluetoothle
I'm just broadcasting a UUID via BLE, and it works. Here is my code:
var data = new Plugin.BluetoothLE.Server.AdvertisementData
{
LocalName = "MyServer",
};
data.ServiceUuids.Add(new Guid("MY_UUID_HERE"));
await this.server.Start(data);
The only problem is that it stops broadcasting once I put the app in the background. And resumes again when I open the app again.
How can I let it continue to broadcast once it's in the background? I read the documentation here:
https://developer.apple.com/library/content/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html
And it says that I have to use the CBCentralManager class to obtain the preservation and restoration feature (so I can keep broadcasting the UUID at all times), but I'm having a hard time translating this to Xamarin/C#.
EDIT
After researching some more, I read that I need to create an instance of CBCentralManager and implement WillRestoreState in the delegate. I did this in the AppDelegate:
[Register("AppDelegate")]
public class AppDelegate : MvxApplicationDelegate, ICBCentralManagerDelegate
{
private IGattServer server = CrossBleAdapter.Current.CreateGattServer();
private CBCentralManager cbCentralManager;
public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
// irrelevant code...
this.Ble();
return true;
}
private async Task Ble()
{
try
{
await Task.Delay(5000); // wait for it to finish initializing so I can access BLE (it crashes otherwise)
var options = new CBCentralInitOptions();
options.RestoreIdentifier = "myRestoreIndentifier";
this.cbCentralManager = new CBCentralManager(this,null,options);
var data = new Plugin.BluetoothLE.Server.AdvertisementData
{
LocalName = "MyServer",
};
data.ServiceUuids.Add(new Guid("MY_UUID_HERE"));
await this.server.Start(data);
}
catch (Exception e)
{
}
}
public void UpdatedState(CBCentralManager central)
{
//throw new NotImplementedException();
}
[Export("centralManager:willRestoreState:")]
public void WillRestoreState(CBCentralManager central, NSDictionary dict)
{
//never gets called
}
But it didn't make a difference for me. And the WillRestoreState method never gets called... I don't mind using a different plugin/library if I have to at this point...
EDIT 2
I just realized that the app is still broadcasting while it is in the background, I just don't see the service UUID anymore (in the web portal of the beacon that I'm testing with), I only see the phone's identifier.
After doing tons of research, I found that it is simply an iOS restriction - you can not broadcast the UUID of a BLE service while your app is in the background. Background work is very restrictive in iOS.
EDIT to include Paulw11 comment (which is true):
You can advertise a service, but it is advertised in a way that only another iOS device that is specifically scanning for that service UUID can see.
Although you can not broadcast the UUID of a BLE service while your iOS app is in the background, for anyone trying to do something similar, you should look into iBeacon. It's Apple's protocol for letting iOS apps do bluetooth stuff while it's in the background.

WCSession transferUserInfo only works in Foreground

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.

Inconsistent behaviour with WatchKit app - Swift

I'm trying to make iOS app to communicate with watch, but i get inconsistent behaviour all the time - either the communication is too slow, or none of the data gets transferred at all.
Besides, i don't see any "Phone disabled" screen when the watchKit runs (which causes a crash, because i need to get data from the phone first).
This is what i have in regards to establishing the WCSession in the iPhone app
App Delegate
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if NSClassFromString("WCSession") != nil {
if #available(iOS 9.0, *) {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
if session.paired {
print("Watch connected")
} else {
print("No watch")
}
}
} else {
}}
if NSClassFromString("WCSession") != nil {
if(WCSession.isSupported()){
session.sendMessage(["b":"delegateSaysHi"], replyHandler: nil, errorHandler: nil)
}}
}
MainViewController
(viewDidLoad)
if NSClassFromString("WCSession") != nil {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
if session.paired {
print("Watch connected")
} else {
print("No watch")
}
}}
MainViewController (Method for transferring bunch of data from iOS app to watchKit app)
func transferData(){
do {
let dataArray = ["somedata": array2d1]
try WCSession.defaultSession().updateApplicationContext(dataArray)
let dataArray1 = ["somedata1": array2d2]
try WCSession.defaultSession().updateApplicationContext(dataArray1)
let dataArray2 = ["somedata2": array2d3]
try WCSession.defaultSession().updateApplicationContext(dataArray2)
let dataArray3 = ["somedata3": array2d4]
try WCSession.defaultSession().updateApplicationContext(dataArray3)
// and up to 12
}
catch {
print("Something wrong happened")
}
}
And this is for watchKit app
App Delegate
func applicationDidFinishLaunching() {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
}
}
func applicationDidBecomeActive() {
if(WCSession.isSupported()){
self.session.sendMessage(["b":"peek"], replyHandler: nil, errorHandler: nil)
}
InterfaceController (awakeWithContext)
if(WCSession.defaultSession().reachable){
self.session.sendMessage(["b":"peek"], replyHandler: nil, errorHandler: nil)
}
Method for receiving ApplicationContext data
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
if let retrievedArray1 = applicationContext["somedata"] as? [[String]] {
self.watchAppArray = retrievedArray1
}
if let retrievedArray2 = applicationContext["somedata2"] as? [[String]] {
self.watchAppArray = retrievedArray1
// and so on for 12 arrays sent from phone
}
}
}}
Any advices on clearing out the situation are very welcome!
Thank you.
Multiple delegates/activations:
You're repeatedly setting up, delegating, and activating sessions in different parts of your app. You keep changing your delegate, so code in one part of the app will no longer be used after you delegated handling to a different part of your app.
You should use a single session/delegate throughout your app. One solution is to setup a WCSession singleton which would be available app-wide. Here's a guide which walks you through that process.
Only the most recent application context would get sent:
By trying to queue up multiple application context requests, the earlier ones would no longer be in the queue when the system gets around to transmitting it, as the system would have already replaced the preceding context with the later one. So only the last (dataArray3) would ever get transmitted.
Use the updateApplicationContext:error: method to communicate recent state information to the counterpart. When the counterpart wakes, it can use this information to update its own state. ... This method overwrites the previous data dictionary, so use this method when your app needs only the most recent data values.
If all of the arrays represent the recent state of your application, you want to transmit them together in a single dictionary.
var dataArray = [String: AnyObject]()
dataArray["somedata"] = array2d1
dataArray["somedata1"] = array2d2
dataArray["somedata2"] = array2d3
dataArray["somedata3"] = array2d4
do {
try session.updateApplicationContext(dataArray)
}
catch {
print(error)
}
It may also help to add some error handling to your sendMessage code, as the paired device may not always be reachable.
Slow communication:
As for the communication being too slow, there are two issues at hand.
Transfers may not happen immediately.
When only one session is active, the active session may still send updates and transfer files, but those transfers happen opportunistically in the background.
Remember that background transfers are not be delivered immediately. The system sends data as quickly as possible but transfers are not instantaneous, and the system may delay transfers slightly to improve power usage. Also, sending a large data file requires a commensurate amount of time to transmit the data to the other device and process it on the receiving side.
The more data you send, the longer it takes to transmit/receive it all.
When sending messages, send only the data that your app needs. All transfers involve sending data wireless to the counterpart app, which consumes power. Rather than sending all of your data every time, send only the items that have changed.
You can control how much data you send, as well as whether the data is sent interactively or in the background. If the watch is reachable, you could use sendMessage for immediate communication. If it's not reachable, you could fall back on a background method.

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