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

1k Views Asked by At

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()
            }
    }
    
    
}

2

There are 2 best solutions below

0
On BEST ANSWER

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.

12
On

Update

After looking at you code I have noticed two main issues:

  1. 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()
    }

}
  1. 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.