Watchkit - didReceiveApplicationContext works only for the first time - ios

When I run an watch connectivity app with iOS and watchOS simulators in Xcode, WCSession delegate method didReceiveApplicationContext works only for the first time, but then it is not called and nothing changes in the Interface controller. Can anyone please explain the reason why is this happening?
Below is WCSessionVC class of UIViewController
import Foundation
import UIKit
import WatchConnectivity
class WCSessionVC: UIViewController, WCSessionDelegate {
let session = WCSession.default
override func viewDidLoad() {
super.viewDidLoad()
session.delegate = self
session.activate()
}
func updateApplicationContext(applicationContext: [String : Any]) throws {
if WCSession.default.isPaired {
do {
try WCSession.default.updateApplicationContext(applicationContext)
} catch let error {
throw error
}
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("Session activated")
let message = ["quote": "Hello"]
do {
try self.updateApplicationContext(applicationContext: message as [String : Any])
}
catch {
print(error)
}
}
}
Below is InterfaceController class of WKInterfaceController
import WatchKit
import Foundation
import WatchConnectivity
class InterfaceController: WKInterfaceController, WCSessionDelegate {
#IBOutlet var lblUserId: WKInterfaceLabel!
var watchSession: WCSession? {
didSet {
if let session = watchSession {
session.delegate = self
session.activate()
}
}
}
override func awake(withContext context: Any?) {
super.awake(withContext: context)
if let session = watchSession {
session.delegate = self
session.activate()
}
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
//loadDataFromDatastore()
watchSession = WCSession.default
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
//MARK: Delegate Methods
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
watchSession?.activate()
print("Session activation did complete")
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
DispatchQueue.main.async {
print("watch received app context: ", applicationContext)
if let data = applicationContext["quote"] as? String {
self.lblUserId.setText(data)
}
}
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print(message)
}
}

The reason for this is because updateApplicationContext only triggers when the contents of the application context dictionary changes. The apple documentation describes it as (emphasis is mine):
Use the updateApplicationContext(_:) method to communicate recent state information to the counterpart. When the counterpart wakes, it can use this information to update its own state. For example, an iOS app that supports Background App Refresh can use part of its background execution time to update the corresponding Watch app. This method overwrites the previous data dictionary, so use this method when your app needs only the most recent data values.
So think of it as a property setter method that only triggers KVO when the value actually changes. Here the receiving side delegate method is only triggered when the contents of the dictionary changes, so in your example above if you change this line:
let message = ["quote": "Hello"]
to be this:
let message = ["quote": "Hello", "date": NSDate()]
You'll see the receiving side delegate gets a callback each time.

Related

iPhone not receiving applicationContext from AppleWatch

I have created a program to test sending data back and forth from iPhone and AppleWatch, and visa versa. I've set it up so there is a button on the AppleWatch and a button on the iPhone. When the iPhone one is pressed, it will send data and rename the button on the AppleWatch to whatever that data String was.
I then implemented the same code for AppleWatch to iPhone but for some reason iPhone doesn't seem to receive the data. Here's the code for iPhone:
// ViewController.swift
import UIKit
import Foundation
import WatchConnectivity
class WatchManager: UIViewController, WCSessionDelegate {
var watchSession: WCSession? {
didSet {
if let session = watchSession {
session.delegate = self
session.activate()
}
}
}
override func viewDidLoad(){
super.viewDidLoad()
watchSession = WCSession.default
}
private func sendDict(_ dict: [String: Any]) {
do {
try self.watchSession?.updateApplicationContext(dict)
} catch {
print("Error sending dictionary \(dict) to Apple Watch!")
}
}
#IBOutlet weak var transferButton: UIButton!
#IBAction func dataTransfer(_ sender: Any) {
sendDict(["DataKey": UUID().uuidString])
print("sent")
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("Session activation did complete")
}
public func sessionDidBecomeInactive(_ session: WCSession) {
print("session did become inactive")
}
public func sessionDidDeactivate(_ session: WCSession) {
print("session did deactivate")
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
print("phone received app context: ", applicationContext)
if let temperature = applicationContext["DataKey"] as? String {
self.transferButton.setTitle(temperature, for: .normal)
}
}
}
and AppleWatch:
// InterfaceController.swift
import WatchKit
import Foundation
import WatchConnectivity
class InterfaceController: WKInterfaceController {
var watchSession: WCSession? {
didSet {
if let session = watchSession {
session.delegate = self
session.activate()
}
}
}
#IBOutlet weak var temperatureLabel: WKInterfaceButton!
private func sendDict(_ dict: [String: Any]) {
do {
try self.watchSession?.updateApplicationContext(dict)
} catch {
print("Error sending dictionary \(dict) to iPhone!")
}
}
#IBAction func button() {
let urg = ["DataKey":UUID().uuidString]
sendDict(urg)
print("watch sent app context \(urg)")
}
}
extension InterfaceController: WCSessionDelegate {
#if os(iOS)
public func sessionDidBecomeInactive(_ session: WCSession) { }
public func sessionDidDeactivate(_ session: WCSession) {
session.activate()
}
#endif
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("Session activation did complete")
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
print("watch received app context: ", applicationContext)
if let temperature = applicationContext["DataKey"] as? String {
self.temperatureLabel.setTitle(temperature)
}
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
watchSession = WCSession.default
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
}
I've tried changing the name of the key (didn't work), I made it so the data value is always changing (UUID().uuidString) and kept that. Other things I tried consisted of creating a label and trying to rename that instead of the button which failed, and lastly instead of renaming the button just sending some confirmation that it received data back to the apple watch, which failed.
Any help would be much appreciated, I hope it's not a silly error.
I think updateApplicationContextis not the right method for your needs.
From the docs:
The system sends context data when the opportunity arises, with the goal of having the data ready to use by the time the counterpart wakes up.
If you want to send data back and forth while both apps are in foreground sendMessage should work.
In the end you may need to implement a combination of both methods. I suggest reading the following doc: https://developer.apple.com/documentation/watchconnectivity/wcsession
Edit:
Just to make the point with "In the end you may need to implement a combination of both methods" even clearer, I have added some sample code from one of my apps.
The method _sendData tries to send the current data via sendMessageData, if the watch is reachable. If not it updates the application context to have the data available as some as the watch app starts.
- (void)_sendCurrentData
{
if ([WCSession defaultSession].isPaired && [WCSession defaultSession].isWatchAppInstalled)
{
if ([WCSession defaultSession].isReachable)
{
// Send data to watch
[[WCSession defaultSession] sendMessageData:self.currentData
replyHandler:nil
errorHandler:^(NSError * _Nonnull error) {
[self _updateApplicationContext];
}];
}
else
{
[self _updateApplicationContext];
}
}
}
- (void)_updateApplicationContext
{
if ([WCSession defaultSession].isPaired && [WCSession defaultSession].isWatchAppInstalled)
{
NSError* error = nil;
[[WCSession defaultSession] updateApplicationContext:#{#"data": self.currentData}
error:&error];
if (error != nil)
{
NSLog(#"error while updating application context: %#", error.localizedDescription);
}
}
}
use self.watchSession?.transferUserInfo(dict) instead of self.watchSession?.updateApplicationContext(dict)
you'll got a call back in:
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
}

Send Name and Birthday to Watch using WatchConnectivity

I’m accessing a users Contacts to send the contact Name and Birthday from the iPhone to the AppleWatch. I have access to the Contacts and can display the data on the phone, but I’m having trouble sending the info with WatchConnectivity. I'm using the Ray Wenderlich book WatchOS by Tutorials as a guide but am still having trouble.
Here is what I have on the Phone side to set up Watch Connectivity in AppDelegate.swift.
// MARK: Watch Connectivity
extension AppDelegate: WCSessionDelegate {
func sessionDidBecomeInactive(_ session: WCSession) {
print("WC Session did become inactive.")
}
func sessionDidDeactivate(_ session: WCSession) {
print("WC Session did deactivate.")
WCSession.default().activate()
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print("WC Session activation failed with error:" + "\(error.localizedDescription)")
return
}
print("Phone activated with state:" + "\(activationState.rawValue)")
}
func setUpWatchConnectivity() {
if WCSession.isSupported() {
let session = WCSession.default()
session.delegate = self
session.activate()
}
}
}
And this in ViewController.swift
// MARK Watch Connectivity
extension ViewController {
func sendNameAndBirthdayToWatch() {
if WCSession.isSupported() {
let session = WCSession.default()
if session.isWatchAppInstalled {
let nameAndBirthday = ["name": nameLabel.text, "birthday": birthdayLabel.text]
do {
try session.updateApplicationContext(nameAndBirthday)
} catch {
print("Error")
}
}
}
}
}
I'm calling sendNameAndBirthdayToWatch() with a UIButton on the phone. As a prototype the phone currently displays the name and birthday on two separate labels pulled from the Contacts in the Xcode simulator so I at least know I'm getting the data.
On the watch in the ExtensionDelegate.swift I have.
// MARK: Watch Connectivity
extension ExtensionDelegate: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print("Activation failed with error: \(error.localizedDescription)")
return
}
print("Watch activated with activation state: \(activationState.rawValue) ")
}
func setupWatchConnectivity() {
if WCSession.isSupported() {
let session = WCSession.default()
session.delegate = self
session.activate()
}
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
if let name = applicationContext["name"] as? String, let birthday = applicationContext["birthday"] as? String {
InterfaceController.sharedInterfaceController.updateWatchLabel(name: name, birthday: birthday)
}
}
}
This is where I'm stuck. I'm not sure where I've gone wrong and/or how to move forward.
I'm calling updateWatchLabel() in the InterfaceController.swift on the watch, but I'm not seeing any results.
Thank you in advance for any help. I've been staring at this for a solid week and unfortunately the example project in the Wenderlich book is a bit too complicated for my understanding.
I'm using Xcode 8.2.1 and Swift 3.
In time I answered my own question.
Here is the code on the phone in ViewController.swift. The code remains the same as in my original post in AppDelegate.swift.
func sendMessageToWatch() {
if WCSession.isSupported() {
let session = WCSession.default()
if session.isWatchAppInstalled {
do {
let message = ["message": "A message as String"]
try session.updateApplicationContext(message)
} catch {
print("Error: \(error)")
}
} else if session.isWatchAppInstalled == false {
print("No watch app installed")
}
}
}
I am at the moment calling this function by pressing a UIButton on the phone. Then on the watch side in InterfaceController.swift for the receiving end. I basically moved the code from ExtensionDelegate.swift to InterfaceController.swift.
// MARK: Watch Connectivity
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print("Activation failed with error: \(error.localizedDescription)")
return
}
print("Watch activated with activation state: \(activationState.rawValue) ")
}
func setupWatchConnectivity() {
if WCSession.isSupported() {
let session = WCSession.default()
session.delegate = self
session.activate()
}
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
let message = applicationContext["message"] as? String
DispatchQueue.main.async(execute: {
self.label.setText(message)
})
}

Send and receive messages from iPhone to Watch from multiple Controllers

What is the proper way to handle communication sent and received from multiple controllers?
What I'm trying to do is send a message from two different viewControllers from iOS to two different interfaceControllers in WatchOS (separately, NOT at the sametime).
Here is what I have which only works for communication between ViewController2 and InterfaceController2, it crashes when a messages is sent from ViewController1 to InterfaceController1 since it appears to be targeting the session method from InterfaceController2 all the time.
ViewController 1:
class ViewController1: UIViewController,WCSessionDelegate{
var session: WCSession!
override func viewDidLoad() {
super.viewDidLoad()
if WCSession.isSupported() {
session = WCSession.default()
session.delegate = self
session.activate()
}
}
func sendDataToWatch(){
let sendPrice:[String: Double] = ["price": 3.99]
session.sendMessage(sendPrice, replyHandler: { replyMessage in
// Some reply here, this could be nil
}, errorHandler: {error in
// Catch any errors here, this could be nil
print("Error: \(error.localizedDescription)")
})
}
}
InterfaceController 1: Receives message form ViewController 1
class InterfaceController1: WKInterfaceController, WCSessionDelegate{
var session: WCSession!
override func awake(withContext context: Any?) {
super.awake(withContext: context)
if (WCSession.isSupported()) {
session = WCSession.default()
session.delegate = self
session.activate()
}
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
/// Capture data from ViewContorller 2
let priceFromPhone = message["price"] as? String
// do something with priceFromPhone
}
}
// ===========================================
ViewController 2:
class ViewController2: UIViewController,WCSessionDelegate{
var session: WCSession!
override func viewDidLoad() {
super.viewDidLoad()
if WCSession.isSupported() {
session = WCSession.default()
session.delegate = self
session.activate()
}
}
func sendDataToWatch(){
let sendEngine:[String: Double] = ["engine": 2.5]
session.sendMessage(sendEngine, replyHandler: { replyMessage in
// Some reply here, this could be nil
}, errorHandler: {error in
// Catch any errors here, this could be nil
print("Error: \(error.localizedDescription)")
})
}
}
InterfaceController 2: Receives message from ViewController 2
class InterfaceController2: WKInterfaceController, WCSessionDelegate{
var session: WCSession!
override func awake(withContext context: Any?) {
super.awake(withContext: context)
if (WCSession.isSupported()) {
session = WCSession.default()
session.delegate = self
session.activate()
}
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
/// Capture data from ViewContorller 2
let engineFromPhone = message["engine"] as? String
// do something with engineFromPhone
}
}
Thanks
I'd suggest removing all data management away from the controllers that handle your UI. It is a poor design and will likely cause you headaches later to mix the layers like this.
You should instead have a data manager that is the WCSession delegate and takes care of persisting the information, and then notifying the relevant parties (view controllers, etc) that the backing data has been updated.
Based on what I have read, it looks like I will need to narrow down the communication to only one ViewController and one InterfaceController and then share the changes via NSNotification or Delegation.
WatchConnectivity how to share session among multiple WKInterfaceControllers?
Using WCSession with more than one ViewController

Apple Watch Not Passing Data to iPhone - Swift

I'm trying to pass a String from my Apple Watch to an iPhone but it seems like it's not connecting. Here's my code:
ViewController.swift :
import UIKit
import WatchConnectivity
class ViewController: UIViewController, WCSessionDelegate {
#IBOutlet weak var lablel: UILabel!
var string = "Hello World"
let session = WCSession.default()
override func viewDidLoad() {
super.viewDidLoad()
session.delegate = self
session.activate()
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
let msg = message["StringValueSentFromiWatch"] as! String
lablel.text = "Message : \(msg)"
print("iphone recieved message")
}
func session(_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?) {
}
func sessionDidBecomeInactive(_ session: WCSession) {
}
func sessionDidDeactivate(_ session: WCSession) {
}
}
InterfaceController.swift :
import WatchKit
import Foundation
import WatchConnectivity
class InterfaceController: WKInterfaceController, WCSessionDelegate {
let session = WCSession.default()
override func willActivate() {
super.willActivate()
session.delegate = self
session.activate()
}
#IBAction func SendPressed() {
//Send Data to iOS
let msg = ["StringValueSentFromiWatch" : "Hello World"]
session.sendMessage(msg, replyHandler: { (replay) -> Void in
print("apple watch sent")
}) { (error) -> Void in
print("apple watch sent error")
}
}
func session(_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?){
}
}
I'm trying to send "Hello World" to the iPhone but I get this printout in the console:
errorHandler: YES with WCErrorCodePayloadUnsupportedTypes
and 'apple watch sent error'.
I know it's not sending but I don't know why. Does anyone know why this doesn't work?
Note: I'm running this is the simulator but I'm fairly sure this is not the problem.
I think you have messed up in the sendMessage(), I cannot work out the replyHandler syntax, and you miss the errorHandler: parameter.
Anyway, I've tried your code, and with a few changes it would work.
1). In InterfaceController, the sendPressed():
var count = 0
#IBAction func SendPressed() {
//Send Data to iOS
let msg = ["Count" : "\(count)"]
if session.isReachable {
session.sendMessage(msg, replyHandler: nil, errorHandler: { (error) -> Void in
print("Error handler: \(error)")
})
count += 1
}
}
I've added a count, since the message must vary for each call (to conserve battery), so you can now press the button several times in a row. And a check to verify that the host application is reachable.
2.) In the ViewController, remember to update the GUI on the main thread:
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
DispatchQueue.main.async {
self.lablel.text = "Message : \(message)"
}
}
Otherwise the label will not update when you receive the data.
Let me know if it helps you!

Easiest way to pass file and user info on watch app start with WatchConnectivity

Have an application that needs to share a file and then a user info dictionary when the watch app becomes active but regardless of whether the iOS app is active. What is the best way to trigger that request from the iPhone to the Watch?
UserDefaults only in WatchOS 1 not in latest WatchOS .
You can share your "userinfo" by enabling the Capabilites of group on both application and watch target and sharing by the Userdefaults among the Targets(iPhone and Watch).
//iPhone sharing Userinfo
func sharedUserInfo() {
if let userDefaults = UserDefaults(suiteName: "group.watch.app.com" ) {
userDefaults.set( userinfo as AnyObject, forKey: "UserInfo")
userDefaults.synchronize()
}
}
//Watch extracting the info
func sharedInfo() {
if let userDefaults = UserDefaults(suiteName: "group.watch.app.com") {
let userInfo = userDefaults.string(forKey: "UserInfo")
}
}
For Watch connectivity we can implement simply by :-
// Watch Side
// InterfaceController.swift
// WatchKit Extension
import WatchKit
import Foundation
import WatchConnectivity
class InterfaceController: WKInterfaceController,WCSessionDelegate {
#IBOutlet var textLabel: WKInterfaceLabel!
var session:WCSession?
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
checkSupportOfSession()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
func checkSupportOfSession() {
if( WCSession.isSupported() ) {
self.session = WCSession.default()
self.session?.delegate = self
self.session?.activate()
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("session")
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
let message:String = message["textIndex"] as! String
textLabel.setText(message)
print(message)
}
}
//Application side Code
import UIKit
import WatchConnectivity
class ViewController: UIViewController,WCSessionDelegate {
#IBOutlet weak var textWord: UITextField!
var session:WCSession?
override func viewDidLoad() {
super.viewDidLoad()
checkSupportOfSession()
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("session")
}
func checkSupportOfSession() {
if( WCSession.isSupported() ) {
self.session = WCSession.default()
self.session?.delegate = self
self.session?.activate()
}
}
#available(iOS 9.3, *)
public func sessionDidBecomeInactive(_ session: WCSession)
{
print("sessionS 2")
}
#available(iOS 9.3, *)
public func sessionDidDeactivate(_ session: WCSession){
}
#IBAction func sendTextToWatch(_ sender: Any) {
print("send text to watch amount")
if let textName = textWord.text {
session?.sendMessage(["textIndex" : textName as String], replyHandler: nil, errorHandler: nil)
}
}
}
https://github.com/shrawan2015/Application-Demo-StackOverFlow/tree/master/WatchOS

Resources