iosios-simulatorwatchoswatchconnectivity

WatchConnectivity: WCSession.transferUserInfo succeeds on a watch but the data never reaches an iPhone


Context

I am building a watchOS app for iOS app (i.e. it's not an independent watchOS app) with Xcode 12.3.

My watchOS app needs to send little chunks of data (structs containing a string and a date) to the iOS as a result of user actions on the watch, then the iOS app will persist the received data in its own storage. The data from the watch doesn't need to be transferred immediately, but it can't be lost either and eventually it must reach the iOS app. I figured that transferUserInfo(_:) method of WCSession best suits my needs.

Problem

When I call transferUserInfo_: on a default WCSession on the watchOS simulator, everything succeeds but the data never reaches the counterpart iOS app.

I use Xcode 12.3, the iOS 14.3 simulator and a paired watchOS 7.2 simulator.

I make sure to activate the WCSession on app startup and check that it's activated successfully and I check if the session is active before attempting to transfer the user info.

Curiously enough, I am able to transfer the data from iOS to watchOS using the updateApplicationContext(_:) method of WCSession, but can't transfer the data back from the watch to the iOS app.

Code

I've built a minimal example illustrating the problem. The watchOS app shows a button and when the user taps it, it is supposed to send a random number to iOS. The iOS app is supposed to display the received number in a text label on the screen.

Here's a full code on the sample iOS app, it's just one file:

import os
import SwiftUI
import WatchConnectivity

class WatchConnectivityService: NSObject, ObservableObject {
    @Published private(set) var receivedNumber: Int
    private let logger = Logger(subsystem: "WCExperimentsiOSApp", category: "WatchConnectivityService")
    private let wcSession: WCSession
    
    override init() {
        self.receivedNumber = -9999
        self.wcSession = WCSession.default
        super.init()
        if WCSession.isSupported() {
            wcSession.delegate = self
            wcSession.activate()
        }
    }
}

extension WatchConnectivityService: WCSessionDelegate {
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        logger.notice("WCSession activationDidCompleteWith state: \(activationState), error: \(error?.localizedDescription ?? "nil")")
    }
    
    func sessionDidBecomeInactive(_ session: WCSession) {
        logger.notice("WCSession sessionDidBecomeInactive")
    }
    
    func sessionDidDeactivate(_ session: WCSession) {
        logger.notice("WCSession sessionDidDeactivate")
    }
    
    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
        logger.notice("Received userInfo: \(userInfo)")
        if let number = userInfo["number"] as? Int {
            DispatchQueue.main.async {
                self.receivedNumber = number
            }
        }
    }
}

@main
struct WatchConnectivityExperimentsApp: App {
    @StateObject var service = WatchConnectivityService()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(service)
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var service: WatchConnectivityService
    var body: some View {
        Text("Received number: \(service.receivedNumber)")
    }
}

Once the iOS app starts up, I can see a log message about a successful activation of the WCSession, it looks like this: "WCSession activationDidCompleteWith state: activated, error: nil".

Here's a full code of the watchOS extension:

import os
import SwiftUI
import WatchConnectivity

class WatchConnectivityService: NSObject, ObservableObject {
    private let wcSession: WCSession
    private let logger = Logger(subsystem: "WCExperimentsWatchApp", category: "WatchConnectivityService")
    
    override init() {
        self.wcSession = WCSession.default
        super.init()
        if WCSession.isSupported() {
            wcSession.delegate = self
            wcSession.activate()
        }
    }
    
    func sendNumberToiOS() {
        guard wcSession.activationState == .activated else {
            logger.error("Error attempting to transfer user info: WCSession is not activated")
            return
        }
        let randomNumber = Int.random(in: 1...1000)
        wcSession.transferUserInfo(["number": randomNumber])
    }
}

extension WatchConnectivityService: WCSessionDelegate {
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        logger.notice("WCSession activationDidCompleteWith state: \(activationState), error: \(error?.localizedDescription ?? "nil")")
    }
    
    func session(_ session: WCSession, didFinish userInfoTransfer: WCSessionUserInfoTransfer, error: Error?) {
        logger.notice("WCSession didFinish userInfoTransfer: \(userInfoTransfer.userInfo), error: \(error?.localizedDescription ?? "nil")")
    }
}

@main
struct WatchConnectivityExperimentsApp: App {
    @StateObject var service = WatchConnectivityService()
    var body: some Scene {
        WindowGroup {
            NavigationView {
                ContentView()
                    .environmentObject(service)
            }
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var service: WatchConnectivityService
    var body: some View {
        VStack {
            Button(action: { service.sendNumberToiOS() }, label: {
                Text("Send random number to iOS app")
            })
        }
    }
}

In the watchOS app, I also see a log message that the WCSession got activated successfully on startup. When I tap the button on the watch simulator, I see that the WCSessionDelegate method func session(_ session: WCSession, didFinish userInfoTransfer: WCSessionUserInfoTransfer, error: Error?) gets called and it reports no errors. For example: "WCSession didFinish userInfoTransfer: ["number": 683], error: nil".

After that, I see the following logs in the Console.app connected to the watch simulator:

Process: wcd (Foundation) Subsystem: com.apple.foundation.filecoordination Category: claims Message: Write options: 8 -- URL: Library/Application<decode: mismatch for [%20S] got [SCALAR sz:8]>upport/com.apple.watchconnectivity/E88632F5-57F5-49F9-9CE8-98C1AA6C31CB/UserInfoTransfers/contents.index -- file:///Users/myusername/Library/Developer/CoreSimulator/Devices/753CCF4F-FFAD-4416-BBC6-E23234D0A500/data/Containers/Data/Application/4802344B-0A89-493C-82B3-60D41C76B8B2/ -- purposeID: 2F068AB7-4DC3-4A75-9745-A74AD9179CA4 -- claimID: FF99C583-0CED-44D0-8403-BD342FCE6CAC

Process: WCExperimentsWatchApp Extension Subsystem: com.apple.foundation.filecoordination Category: claims Message: Read options: 1 -- URL: Library/Application<decode: mismatch for [%20S] got [SCALAR sz:8]>upport/com.apple.watchconnectivity/E88632F5-57F5-49F9-9CE8-98C1AA6C31CB/UserInfoTransfers/contents.index -- file:///Users/myusername/Library/Developer/CoreSimulator/Devices/753CCF4F-FFAD-4416-BBC6-E23234D0A500/data/Containers/Data/Application/4802344B-0A89-493C-82B3-60D41C76B8B2/ -- purposeID: D53FBF3C-8B2C-48FB-9390-9058CAD4C757 -- claimID: 62292E97-3529-415A-B6B3-1768387D67B4

On the iOS side, the WCSessionDelegate method session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) never gets called and the iOS app never receives any data from the watch.

I tried different simulators, tried resetting all content and settings of the simulators and it had no effect. Unfortunately I'm unable to test it using a real Apple Watch because I don't have one.

Could someone please suggest whether I'm doing something incorrectly? How do I correctly send userInfo from the watch to the iPhone?

Thanks in advance.


Solution

  • Apple's docs finally include the following warning:

    Warning

    Always test Watch Connectivity data transfers on paired devices. The Simulator app doesn’t support the transferUserInfo(_:) method.

    This is also true for transferFile(_:metadata:).