I am building an iOS app that includes a Safari Web Extension and a companion Apple Watch App.
My goal is simple: When the user taps the button in the Safari Extension popup, I want to send a message (e.g., the current timestamp) to the Apple Watch.
The Setup:
-
iOS 26.2 / Xcode 26.2
-
Target 1: iOS Main App (activates WCSession successfully).
-
Target 2: Safari Web Extension (Manifest V3, using SafariWebExtensionHandler).
-
Target 3: Watch App.
The Problem:
While WatchConnectivity works perfectly from the main iOS App, it fails immediately when called from the SafariWebExtensionHandler. It seems like the extension process is sandboxed in a way that prevents it from connecting to the Watch Connectivity Daemon (com.apple.wcd).
The Trigger (JavaScript):
The Swift logic is triggered via Native Messaging from the extension’s popup (popup.js) when the user taps a button. I have the “nativeMessaging” permission enabled in my manifest.json.
// popup.js
browser.runtime.sendNativeMessage({ message: "sendTime" }, function(response) {
console.log("Response from Swift:", response);
});
The Code (Extension Handler):
I am using a Singleton to manage the session to avoid race conditions during the short lifecycle of the extension.
import SafariServices
import WatchConnectivity
import os.log
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling, WCSessionDelegate {
func beginRequest(with context: NSExtensionContext) {
// ... extracting message from JS ...
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate() // <--- FAILS HERE
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if activationState == .activated {
session.transferUserInfo(["time": "12:00"])
}
}
// ... other delegate stubs ...
}
The Logs:
The logs clearly show an XPC interruption immediately after trying to connect to com.apple.wcd. It retries 5 times and then fails.
WatchTestExt Extension ... [0x1030d0640] activating connection: mach=true ... name=com.apple.wcd
WatchTestExt Extension ... [0x1030d0640] Re-initialization successful; calling out to event handler with XPC_ERROR_CONNECTION_INTERRUPTED
WatchTestExt Extension ... -[WCSession xpcConnectionInterrupted]
WatchTestExt Extension ... -[WCSession activateSession]_block_invoke sessionReadyForInitialStateWithCompletionHandler failed due to NSXPCConnectionInterrupted
...
WatchTestExt Extension ... -[WCXPCManager onqueue_retryConnectIfNecessary] failed to reconnect to daemon (5 attempts)
What I have tried:
-
Direct Communication: Using sendMessage and transferUserInfo directly from the Extension. Result: NSXPCConnectionInterrupted.
-
App Groups (Relay):
-
I successfully implemented a relay using App Groups (UserDefaults(suiteName: …)).
-
The Extension writes the data to the shared group.
-
The Main iOS App observes the UserDefaults (via KVO or Darwin Notifications) and sends the data to the Watch.
-
Issue: This only works if the iOS Main App is running in the background. If the user force-quits the main app (or iOS suspends it aggressively), the relay stops working because the Extension cannot wake up the Main App.
-
My Question:
Is it technically possible for a Safari Web Extension on iOS to communicate directly with WatchConnectivity? Or is this a hard sandbox limitation by Apple?
If it is a hard limitation, is there any way for a Safari Extension to wake up its parent iOS App in the background to perform the transfer, even if the parent app has been killed?