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
Related
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] = [:]) {
}
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.
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!
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
I've been using NatashaTheRobot singleton for the WCSession, but can't get the sendMessage to work properly.
My goal is to send a message from Watch app to the iOS app and transfer a dictionary from iOS app to watch app.
Here's my code in the ExtensionDelegate
import WatchKit
import WatchConnectivity
class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate {
var session:WCSession!
var boolCheck = Int()
func applicationDidFinishLaunching() {
WatchSessionManager.sharedManager.startSession()
print("Here i am")
}
func applicationDidBecomeActive() {
print("I AWOKE")
}
func applicationWillResignActive() {
}
}
class WatchSessionManager: NSObject, WCSessionDelegate {
static let sharedManager = WatchSessionManager()
private override init() {
super.init()
}
private let session: WCSession = WCSession.defaultSession()
func startSession() {
session.delegate = self
session.activateSession()
if WCSession.isSupported(){
self.session.sendMessage(["b":"peek"], replyHandler: nil, errorHandler: nil)
print("works")
} else {
print("don't work")
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject]) {
let sweetN = message["b"]! as? String
dispatch_async(dispatch_get_main_queue(), {
if sweetN == "insertData1" {
NSNotificationCenter.defaultCenter().postNotificationName("sweetData1", object: nil)
})
}
func sendMessage(message: [String : AnyObject],
replyHandler: (([String : AnyObject]) -> Void)? = nil,
errorHandler: ((NSError) -> Void)? = nil)
{
session.sendMessage(message, replyHandler: replyHandler, errorHandler: errorHandler)
print("this is message \(replyHandler)")
var pretty = replyHandler
}
Here's my code in the WCSingleton in the iOS app (separate from the AppDelegate)
import WatchConnectivity
#available(iOS 9.0, *)
class WatchSessionManager: NSObject, WCSessionDelegate {
static let sharedManager = WatchSessionManager()
private override init() {
super.init()
}
private let session: WCSession? = WCSession.isSupported() ? WCSession.defaultSession() : nil
private var validSession: WCSession? {
if let session = session where session.paired && session.watchAppInstalled {
return session
}
return nil
}
func startSession() {
session?.delegate = self
session?.activateSession()
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject]) {
//receieve messages from watch
print(message["b"]! as? String)
let sweetN = message["b"]! as? String
dispatch_async(dispatch_get_main_queue(), {
if sweetN == "peek"{
NSNotificationCenter.defaultCenter().postNotificationName("giveMeInfo", object: nil)
}
}
})
}
#available(iOS 9.0, *)
extension WatchSessionManager {
func sendMessage(message: [String : AnyObject],
replyHandler: (([String : AnyObject]) -> Void)? = nil,
errorHandler: ((NSError) -> Void)? = nil)
{
session!.sendMessage(message, replyHandler: replyHandler, errorHandler: errorHandler)
}
}
and here's the method i use in the ViewController (fired from NSNotificationCenter). However this part of code never gets executed (which is strange, because when i use applicationContext it works perfectly).
func giveMeInfo(){
let linesAdd1 = linesAdd as! AnyObject
WatchSessionManager.sharedManager.sendMessage(["a":linesAdd1])
}
Any insights of how to get all those parts working together are very welcome!
Your Code is a bit confusing to me and looks right but you will run into troubles with this approach for the following reasons:
if you receive the notification in the ViewController and the Watch changes into the inactive state, the sendMessage() method will not work to send data back:
Calling this method from your WatchKit extension while it is active and running wakes up the corresponding iOS app in the background and makes it reachable. Calling this method from your iOS app does not wake up the corresponding WatchKit extension. If you call this method and the counterpart is unreachable (or becomes unreachable before the message is delivered), the errorHandler block is executed with an appropriate error. The errorHandler block may also be called if the message parameter contains non property list data types.
if you want to get data back, then you should use a reply block.
But in your configuration these blocks will not be called because:
sendMessage(reply==nil) --> didReceiveMessage(... message: )
sendMessage(reply!=nil) --> didReceiveMessage(... message: replyHandler:)
on the other hand if you use the contextMethod:
Use this method to transfer a dictionary of data items to the counterpart app. 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. The counterpart’s session delivers the data to the session:didReceiveUpdate: method of its delegate. A counterpart can also retrieve the data from the receivedApplicationContext property of its session.
I hope this helps ;)