WatchConnectivity not activating on iOS side when running on Simulator - ios

I am trying to run my project on Simulator but the iOS side does not activate and only the Watch side manages to activate.
Here is my ScoresInterfaceController.swift (Watch side)
import WatchConnectivity
class ScoresInterfaceController: WKInterfaceController, WCSessionDelegate {
// Used to send information to the iOS app
var applicationDict = [String: Int]()
// Starts a session to communicate with the iOS app
var session: WCSession!
// For WCSession
override init() {
super.init()
if(WCSession.isSupported()) {
session = WCSession.default()
session.delegate = self
session.activate()
}
}
func session(_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?) {}
Here is my ScoreViewController.swift (iOS side)
import WatchConnectivity
class ScoreViewController: UIViewController, WCSessionDelegate {
// Starts a session to communicate with the Watch app
var session: WCSession!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if(WCSession.isSupported()) {
session = WCSession.default()
session.delegate = self
session.activate() //Not activating when run on Simulator
}
}
// For WCSession
/** Called when the session has completed activation. If session state is WCSessionActivationStateNotActivated there will be an error with more details. */
#available(iOS 9.3, *)
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {}
// Receives data from Watch app
#nonobjc func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {}
func sessionDidBecomeInactive(_ session: WCSession) {}
func sessionDidDeactivate(_ session: WCSession) {
WCSession.default().activate()
}
}
Here is the error message:
I followed this tutorial but I cannot figure out what the issue is:
http://kristina.io/watchos-2-how-to-communicate-between-devices-using-watch-connectivity/

Changed my ScoreViewController's definition of session from this:
#nonobjc func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {}
to this:
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any])

Related

WatchConnectivity: didReceiveApplicationContext never gets called although context is updated

I'm stuck with the following problem: I'm able to start WCSessions on both iPhone and Watch (simulator and real devices) and can use the sendMessage method just fine. I'm aware that updateApplicationContext requires an updated dict to be sent, so I'm adding an uuid for debugging purposes:
context = ["user":id,"messageId": UUID().uuidString]
There's no error thrown when the method is called but on the watch side didReceiveApplicationContext never gets called and the receivedApplicationContext dict stays empty all the time. After reading a lot of similar code examples and the docs I can't see where I'm wrong.
I'm building with XCode 12.5 and iOS 14.5 and watchOS 7.4
Here's the iOS code dealing with the WCSession, context is set in another method and is successfully transferred with sendMessage but not with updateApplicationContext:
import WatchConnectivity
public class CDVSettings : CDVPlugin, WCSessionDelegate {
var wcSession : WCSession! = nil
var didSendMessage:Bool = false
var context: [String : Any] = [:] {
didSet {
debugPrint("context didSet:", self.context)
debugPrint("WCSession.isPaired: \(wcSession.isPaired), WCSession.isWatchAppInstalled: \(wcSession.isWatchAppInstalled)")
if wcSession.activationState == WCSessionActivationState.activated {
do {
debugPrint("updateApplicationContext is called")
self.didSendMessage=true
try wcSession.updateApplicationContext(self.context)
}
catch let error as NSError {
debugPrint(error.localizedDescription)
}
catch {}
wcSession.sendMessage(self.context, replyHandler: { reply in
print("Got reply: \(reply)")
}, errorHandler: { error in
print("error: \(error)")
})
} else {
print("activationState is not activated")
wcSession.activate()
}
}
}
public func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("activationDidCompleteWith activationState:\(activationState) error:\(String(describing: error))")
}
public func sessionDidBecomeInactive(_ session: WCSession) {
}
public func sessionDidDeactivate(_ session: WCSession) {
}
#objc(pluginInitialize)
public override func pluginInitialize() {
wcSession = WCSession.default
wcSession.delegate = self
wcSession.activate()
}
[...]
}
And here's the watch part:
import WatchConnectivity
class ConnectivityRequestHandler: NSObject, ObservableObject, WCSessionDelegate {
var session = WCSession.default
override init() {
super.init()
session.delegate = self
session.activate()
debugPrint("ConnectivityRequestHandler started with session", session)
}
// MARK: WCSession Methods
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
debugPrint(error)
debugPrint("session is reachable:",session.isReachable)
debugPrint("last received application context:",session.receivedApplicationContext)
}
func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: #escaping ([String: Any]) -> Void) {
debugPrint("didReceiveMessage: \(message)")
replyHandler(["message received": Date()])
}
func session(_ session: WCSession, didReceiveMessageData messageData: Data, replyHandler: #escaping (Data) -> Void) {
debugPrint("didReceiveMessageData: \(messageData)")
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
debugPrint("did receive application context")
debugPrint(applicationContext)
if let id = applicationContext["user"] as? [String]{
debugPrint("\(id)")
UserDefaults.standard.set(id, forKey: "user")
}
}
}
The corresponding logs on iOS
"context didSet:" ["user": "0e4a28b5-f8a0-40a5-942d-5f13b610a93a",
"messageId": "8A78246C-91E6-48DC-B55D-4F4EBC761211"]
"WCSession.isPaired: true, WCSession.isWatchAppInstalled: true"
"updateApplicationContext is called" Got reply: ["message received":
2021-09-03 08:31:54 +0000]
and on watchOS
"ConnectivityRequestHandler started with session" <WCSession:
0x600003be0b40, hasDelegate: YES, activationState: 0> nil "session is
reachable:" true "last received application context:" [:]
"didReceiveMessage: ["user": 0e4a28b5-f8a0-40a5-942d-5f13b610a93a,
"messageId": 8A78246C-91E6-48DC-B55D-4F4EBC761211]"
What am I doing wrong? Thank you so much for your help.
For those stumbling over my question while researching, it's no real solution, but an observation that I (now) share with others (see comments above): With real devices it works as expected, but you may need to reboot them in the process. In the simulators it seems that different methods may work while others won't and that even differs from developer to developer.
In Xcode 14 this should now also work for Simulator. At least for me it seems to work without problems

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.

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.

WCSession communication delayed/unreliable in iOS/watchOS simulator

After upgrading to XCode 8, I had an app update rejected because a button in the watch app wasn't having an effect on the iOS app. Re-running in the simulator, it seemed that sending messages via WCSession was occasionally failing without error on one machine, and almost always on another I use.
I've created a simple app that replicates the behavior, with a button and counter in both the iOS and watch apps. The intention is that tapping the button in the iOS app will increment the counter in the watch app and vice versa.
A few times when I've left the apps running a while, the messages were eventually delivered (after about 2 minutes).
iOS App ViewController:
class ViewController: UIViewController, WCSessionDelegate {
var count: Int = 0
var watchSession: WCSession?
#IBOutlet var countLabel: UILabel?
#IBAction func sendMessage(sender: AnyObject) {
watchSession?.sendMessage(
["message":"increment"],
replyHandler: { (message: [String : Any]) in
},
errorHandler: { (err: Error) in
})
}
override func viewDidLoad() {
super.viewDidLoad()
if(WCSession.isSupported()){
watchSession = WCSession.default()
watchSession!.delegate = self
watchSession!.activate()
}
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
count+=1
updateView()
replyHandler([:])
}
func updateView(){
DispatchQueue.main.async {
self.countLabel?.text = ("\(self.count)")
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {}
func sessionDidBecomeInactive(_ session: WCSession){}
func sessionDidDeactivate(_ session: WCSession){}
}
watchOS InterfaceController:
class InterfaceController: WKInterfaceController, WCSessionDelegate {
var count: Int = 0
var watchSession : WCSession?
#IBOutlet var countLabel: WKInterfaceLabel?
#IBAction func sendMessage(sender: AnyObject) {
watchSession?.sendMessage(
["message":"increment"],
replyHandler: { (message: [String : Any]) in
},
errorHandler: { (err: Error) in
})
}
override func willActivate() {
super.willActivate()
if(WCSession.isSupported()){
watchSession = WCSession.default()
watchSession!.delegate = self
watchSession!.activate()
}
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
count+=1
updateView()
replyHandler([:])
}
func updateView(){
countLabel?.setText("\(self.count)")
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {}
}

Resources