Check if Apple Watch is not currently connected - ios

I am trying to detect if a user's Apple watch is currently active using this code:
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
But even after I power off the watch, I get a callback to session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) with activationState == .activated.
Is this expected? Is there any way to actually check if the watch and the iPhone are connected?
EDIT: I also note that if I run the same code inside a simulator, I get the exact same callback with activationState == .activated, yet isPaired == false. It sounds to me like .activated basically is the default state. I'm not even sure how to get .deactivated
EDIT 2: I actually do not have a WatchKit extension and would rather not create one for simplicity. I am simply trying to detect if a paired watch is in range, powered on and communicating so I can tell if my apps local notifications might be delivered to the watch.

You are looking for: isPaired property of WCSession.
After calling activateSession and getting callback you can check if Watch is paired there.
If you need to know if app is installed on Watch - check isWatchAppInstalled.

EDIT
I understand what you're asking now. The issue is that if you check isPaired you get true, even if the app is launched and the watch is not connected.
Example:
import WatchConnectivity
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
return true
}
}
extension AppDelegate: WCSessionDelegate {
func session(
_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?) {
switch session.activationState {
case .notActivated:
print("Session no activated")
case .inactive:
print("Session inactive")
case .activated:
print("Session activated")
#unknown default:
break
}
if let err = error {
print("ERROR: \(err.localizedDescription)")
}
if session.isPaired {
print("Current phone is paired to an apple watch!")
}
print("reachable: \(session.isReachable)")
}
func sessionDidBecomeInactive(_ session: WCSession) {
print("Session just became inactive")
}
func sessionDidDeactivate(_ session: WCSession) {
print("session did deactivate")
}
}
This results in the following output with the watch powered on or off
Session activated
Current phone is paired to an apple watch!
reachable: false
Note isReachable is always false because you don't have a watch extension.
Looking at this question there is no solution to this problem. I don't think you can determine this information without a watch extension / app.
Also note that you cannot access the WKInterfaceDevice outside of the watch app.

Related

How do I transfer a dictionary with transferUserInfo to Apple Watch?

I am trying to put a part of my Apple Watch app behind a paywall. For that, the iOS app automatically creates a dictionary with a true/false value, whether the content is purchases of not. The problem is, is no matter how I try, I cannot pass it to the Watch.
Here is my iOS ViewController:
import WatchConnectivity
class ViewController: UIViewController, WCSessionDelegate {
override func viewDidLoad() {
super.viewDidLoad()
}
//The dictionary to be passed to the Watch
var dictionaryToPass = ["product1": 0, "product2": 0]
//This will run, if the connection is successfully completed.
//BUG: After '.activate()'-ing the session, this function successfully runs in the '.activated' state.
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("WCSession - activationDidCompleteWith:", activationState, "and error code:", error as Any)
switch activationState {
case .activated:
print("WCSession - activationDidCompleteWith .activated")
//session.transferUserInfo(dictionaryToPass)
case .inactive:
print("WCSession - activationDidCompleteWith .inactive")
case .notActivated:
print("WCSession - activationDidCompleteWith .notActivated")
default:
print("WCSession - activationDidCompleteWith: something other ")
break
}
}
func sessionDidBecomeInactive(_ session: WCSession) {
print("WCSession - sessionDidBecomeInactive")
}
func sessionDidDeactivate(_ session: WCSession) {
print("WCSession - sessionDidDeactivate")
}
//Pushing the button on the iOS storyboard will attempt iOS-watchOS connection.
#IBAction func tuiButton(_ sender: UIButton) {
let session = WCSession.default
if session.isReachable {
session.transferUserInfo(dictionaryToPass)
} else if WCSession.isSupported() {
session.delegate = self
session.activate()
}
}
#IBAction func sendmButton(_ sender: UIButton) {
let session = WCSession.default
if session.isReachable {
session.sendMessage(dictionaryToPass, replyHandler: { reply in
print(reply)
}, errorHandler: nil)
} else if WCSession.isSupported() {
session.delegate = self
session.activate()
}
}
}
And that's what I have on the watchOS's Interface Controller:
import WatchConnectivity
class InterfaceController: WKInterfaceController, WCSessionDelegate {
//The text label on the Watch Storyboard. Helps with debugging.
#IBOutlet weak var helloLabel: WKInterfaceLabel!
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("watchOS - activationDidCompleteWith:", activationState)
}
//Whatever arrives, it will get printed to the console as well as the 'helloLabel' will be changed to help the debugging progress.
//BUG: This is the part, that never gets run, even tough the WCSession activated successfully.
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
print("watchOS - didReceiveUserInfo", userInfo)
helloLabel.setText("didReceiveUserInfo")
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print("watchOS - didReceiveMessage", message)
helloLabel.setText("didReceiveMessage")
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
replyHandler(["does it work?": "yes sir"])
print("watchOS - didReceiveMessage", message)
helloLabel.setText("didReceiveMessage")
}
//Setting the Interface Controller as WCSession Delegate
private var session: WCSession = .default
override func awake(withContext context: Any?) {
session.delegate = self
session.activate()
}
//Activating the session on the watchOS side as well.
override func willActivate() {
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
}
}
Update
After looking at you code I have noticed two main issues:
You are not setting your InterfaceController as a WCSession delegate. The connection needs to be activated from both ends.
class InterfaceController: WKInterfaceController, WCSessionDelegate {
private var session: WCSession = .default
override func awake(withContext context: Any?) {
session.delegate = self
session.activate()
}
}
To be able to receive a message from the counterpart device, you need to implement the session(_:didReceiveMessage:replyHandler:) method. Add these methods to your InterfaceController:
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print("watchOS - didReceiveMessage", message)
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
replyHandler(["does it work?": "yes sir"])
print("watchOS - didReceiveMessage", message)
}
As you can see, I have also implemented the second funciton that can respond with a replyHandler by calling it a passing some data. This can be useful while debugging.
Update both your button action and a sendMessage call. No need to reactivate a connection it the device is already reachable, also pass a reply handle to make sure watch gives back the data.
#IBAction func button(_ sender: UIButton) {
if session.isReachable {
session.sendMessage(watchInAppPurchases, replyHandler: { reply in
print(reply)
}, errorHandler: nil)
} else if WCSession.isSupported() {
session.delegate = self
session.activate()
}
}
Initial Answer
Do not attempt to sync the data directly after calling activate() since there is no guarantee that the connection is already established. Documentation clearly states that:
This method executes asynchronously and calls the session(_:activationDidCompleteWith:error:) method of your delegate object upon completion.
Since you do set self as a delegate, try to move the transferUserInfo call to the session(_:activationDidCompleteWith:error:) implementation.
func session(
_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?
) {
switch activationState {
case .activated:
session.transferUserInfo(watchInAppPurchases)
default:
// handle other states
break
}
}
Also, when working with Swift make sure to restrain from using CapitalizedCamelCase names for properties, functions etc. Only use this notation for types. I have converted the original WatchInAppPurchases to watchInAppPurchases in the code sample above.
If your call to transferUserInfo still does not work, try to call the sendMessage(_:replyHandler:errorHandler:) instead
switch activationState {
case .activated:
session.sendMessage(watchInAppPurchases, replyHandler: nil, errorHandler: nil)
default:
// handle other states
break
}
and monitor the session(_:didReceiveMessage:replyHandler:) in you watch extension for any incoming messages.
Turns out it was the watchOS simulator that was buggy. Quite a pity one from Apple.
Further reading on Apple's forum: https://developer.apple.com/forums/thread/127460
If anyone else is in the same shoes, I recommend running the code on a physical device, it works perfectly there. The final working code can be found here if anyone from Google results is looking for that.

WC WCSession counterpart app not installed

Making a connection between iOS and iWatch devices, xCode writes [WC] WCSession counterpart app not installed.
After a lot of research, I've found a solution, maybe it will be helpful for someone.
- Check your WatchKit Extention target.
- Uncheck "Supports Running Without iOS App Installation"
- First run iwatch simulator, than run ios simulator with checkmark
I have spent around 3-4 hr to fix this issue, it seems that somehow my Apple watch.app is missing from my Target > Frameworks so after clicking plus icon and adding it back, it doesn't report "WC WCSession counterpart app not installed" anymore
Finally, I made it. For anyone else, who has this problem:
In iOS Target: Make sure in General>Frameworks, Libraries & Embedded Contents> Add YourWatchApp.app if it's not in the list. (For the loop problem, do the next step)
In Watch Target: Go to BuildPhases>Copy Bundle Resources> and remove YouriOSApp.app from the list if exists.
You must set delegate and activate it from both WatchApp and iOSApp as follows:
self.session = WCSession.default
self.session.delegate = self
self.session.activate()
if you are using a helper class, Your class should implement NSObject as well:
class ConnectivityHelper: NSObject, WCSessionDelegate {
Make sure there is some seconds between calling activate method and sending message.
If you are using a reply handler when sending message from Watch, Then you must implement didReceiveMessage method in iOS App which has replyHandler, else replyHandler on the watch returns an error and the iOS app won't receive the message.
Check out this complete code that is working: iOS Part:
import WatchConnectivity
class PhoneConnectivity: NSObject {
override init() {
super.init()
let session = WCSession.default
session.delegate = self
session.activate()
}
}
extension PhoneConnectivity: WCSessionDelegate {
//MARK: Delegate Methodes
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
func sessionDidBecomeInactive(_ session: WCSession) {
}
func sessionDidDeactivate(_ session: WCSession) {
}
// Receiver
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
//without reply handler
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
//with reply handler
}
// end Receiver
}
send message with a test string first like:
WCSession.default.sendMessage(["TEST", "TEST2"], replyHandler: { dictionary in
print(">WC> reply recieved: \(dictionary.first!.value)")
}, errorHandler: { error in
print(">WC> Error on sending Msg: \(error.localizedDescription)")
})
Most developers suggest that make session active ASAP (in iOS using didFinishLaunchingWithOptions in appDelegate of iOSApp & applicationDidFinishLaunching in WKExtensionDelegate of WatchApp)
It only worked for me by installing the Watch App from the watch settings app.
If I install the Watch App from xcode, the iOS app will give me the companion error message.

session(_:activationDidCompleteWith:error:) not called on Apple Watch (but is called in Simulator)

I've created a simple swift class:
import Foundation
import WatchConnectivity
class WatchCommunication : NSObject, WCSessionDelegate {
var session: WCSession!
override init() {
super.init()
if WCSession.isSupported() {
print("WCSession is supported")
self.session = WCSession.default
self.session.delegate = self
if session.activationState == WCSessionActivationState.activated {
print("activationState is activated")
} else {
print("activationState is not activated")
self.session.activate()
}
} else {
print("WCSession is not supported")
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("activationState", activationState.rawValue)
print(error)
}
}
In my Watch App Extension I load create an instance of this class
class ExtensionDelegate: NSObject, WKExtensionDelegate {
let watchCommunication: WatchCommunication = WatchCommunication()
// ...
When I test this code in the simulator, I see the following logged
WCSession is supported
activationState is not activated
activationState 2
nil
All working fine.
When I run the same application on my testing iPhone X and paired Apple Watch 3, the logs show
WCSession is supported
activationState is not activated
So it seems that the method
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
is never called on my Apple Watch.
Methods like
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
are also not called on device (but are working on the simulator).
On the iPhone X side of things, the actionDidCompleteWith method is called, with activationState 2 (activated) and updateApplicationContext does not throw an error.
There is however communication between the iPhone and Apple Watch, because the method https://developer.apple.com/documentation/healthkit/hkhealthstore/1648358-startwatchapp does indeed start a workout on the watch (and all Apple and third party watch apps work normally).
For further reference:
Restarting my Apple Watch (what I never do otherwise) did solve the issue...

How to get UserDefaults data to Apple Watch even if iPhone is not active

I need to get a number from userDefaults to use in an Apple Watch app to make some calculations, I'm using the WatchConnectivity framework to get this piece of information but what I don't like about the way I have it right now is that the Phone only sends the data to the Watch when the iPhone app is loaded (viewDidLoad), in other words is I launch the Watch app I need to open the iPhone app in order to get the data to the Apple Watch.
Is it possible to get data from iPhone when it is not active?
Here is the code I'm using:
iOS View Controller
class ViewController: UIViewController, WCSessionDelegate {
var session: WCSession!
override func viewDidLoad() {
super.viewDidLoad()
if WCSession.isSupported() {
session = WCSession.default()
session.delegate = self
session.activate()
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState:WCSessionActivationState, error: Error?) {}
func sessionDidDeactivate(_ session: WCSession) { }
func sessionDidBecomeInactive(_ session: WCSession) { }
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
// Reply to watch with data from user defaults
replyHandler(["dataFromiPhone": readDoubleFromUserDefaults()])
}
}
WatchOS InterfaceController
class InterfaceController: WKInterfaceController, WCSessionDelegate{
var session: WCSession!
var myNumber:Double = 0
override func willActivate() {
super.willActivate()
if (WCSession.isSupported()) {
session = WCSession.default()
session.delegate = self
session.activate()
}
getDataFromiPhone()
}
override func didDeactivate() {
super.didDeactivate()
}
/// Delegate required method
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {}
func getDataFromiPhone(){
//Send Message to phone - I'm not sure if I need this message
let messageToSend = ["askiPhone":"Hi Phone, send me data from userDefaults."]
session.sendMessage(messageToSend, replyHandler: { replyMessage in
/// handle the reply
let dataFromPhone = replyMessage["dataFromiPhone"] as? String
DispatchQueue.main.async {
self.myNumber = Double(dataFromPhone!)!
}
}, errorHandler: {error in
/// catch any errors here
print("ERROR: \(error)")
})
}
}
I would recommend using App Groups for this. Add an App Group under "Capabilities" for your app's target and the watch extension's target:
And then set your UserDefaults using that App Group:
let appGroupName = "group.mobilemind.SpeedDial"
let appGroupUserDefaults = UserDefaults(suiteName: appGroupName)!
appGroupUserDefaults.set(NSNumber(value: newValue), forKey: "hasRated")
appGroupUserDefaults.synchronize()
Use UserDefaults this way on the app and the watch extension and both will be able to get and set the UserDefaults for the app group.
Edit
This approach does not work on WatchOS 2.0+. However, this approach still works on other types of app extensions.

Watch Networking Broken In Xcode 8 Beta 3

I'm receiving the following error when sending any kind of http request from my WatchKit Extension:
WatchKit Extension[6128:479936] [WC] __33-[WCXPCManager onqueue_reconnect]_block_invoke error reconnecting to daemon due to NSXPCConnectionInterrupted
The message is only attempted to be sent if the session is reachable, which it is at this point. However when I inspect the session object I can see that while reachable is true and the activationState is 2 (WCSessionActivationStateActivated), other properties such as paired and watchAppInstalled are actually false.
In fact, the error is just repeatedly sent multiple times a second while I'm using the app in simulator or device. I'm not sure what's going on but I only started receiving this error when using Xcode 8 Beta 3.
App Delegate:
func session(_ session: WCSession, activationDidCompleteWith 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.
WCSession.default().activate()
}
Extension Delegate:
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
if (WCSession.isSupported()) {
let session = WCSession.default()
session.delegate = self
session.activate()
print("activating")
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: NSError?) {
// ...
}
Interface controller
if WCSession.defaultSession().reachable { // ... }
How can I solve, or at least work around this issue?

Resources