I'm currently building an Apple Watch Companion App with Swift and Flutter. I'm doing this with the help of theamorn's Github project. Everything works in the simulator (iOS 15.0 and WatchOS 8.0), even if the iOS App is force quit. However, when testing on my AW Series 3 (WatchOS 8.0) and iPhone 11 (iOS 15.0) it will only work, as long as the iOS App is opened.
My AppDelegate.swift of the iOS App
import UIKit
import Flutter
import WatchConnectivity
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
var session: WCSession?
let methodChannelName: String = "app.controller.watch"
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
initFlutterChannel()
if WCSession.isSupported() {
session = WCSession.default;
session!.delegate = self;
session!.activate();
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func initFlutterChannel() {
if let controller = window?.rootViewController as? FlutterViewController {
let channel = FlutterMethodChannel(
name: methodChannelName,
binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler({ [weak self] (
call: FlutterMethodCall,
result: @escaping FlutterResult) -> Void in
switch call.method {
case "flutterToWatch":
guard let watchSession = self?.session, watchSession.isPaired,
watchSession.isReachable, let methodData = call.arguments as? [String: Any],
let method = methodData["method"], let data = methodData["data"] as? Any else {
result(false)
return
}
let watchData: [String: Any] = ["method": method, "data": data]
watchSession.sendMessage(watchData, replyHandler: nil, errorHandler: nil)
result(true)
default:
result(FlutterMethodNotImplemented)
}
})
}
}
}
extension AppDelegate: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
func sessionReachabilityDidChange(_ session: WCSession) {
print("Watch reachability: \(session.isReachable)")
if (session.isReachable) {
//invoke sendWakeupToFlutter via MethodChannel when reachability is true
DispatchQueue.main.async {
if let controller = self.window?.rootViewController as? FlutterViewController {
let channel = FlutterMethodChannel(
name: self.methodChannelName,
binaryMessenger: controller.binaryMessenger)
channel.invokeMethod("sendWakeupToFlutter", arguments: [])
}
}
}
}
func sessionDidBecomeInactive(_ session: WCSession) {
}
func sessionDidDeactivate(_ session: WCSession) {
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
DispatchQueue.main.async {
if let method = message["method"] as? String, let controller = self.window?.rootViewController as? FlutterViewController {
let channel = FlutterMethodChannel(
name: self.methodChannelName,
binaryMessenger: controller.binaryMessenger)
channel.invokeMethod(method, arguments: message)
}
}
}
}
My WatchViewModel.swift in my Watch Extension
import Foundation
import WatchConnectivity
class WatchViewModel: NSObject, ObservableObject {
var session: WCSession
var deviceList: String = ""
@Published var loading: Bool = false
@Published var pubDeviceList: [Device]?
// Add more cases if you have more receive method
enum WatchReceiveMethod: String {
case sendLoadingStateToNative
case sendSSEDeviceListToNative
}
// Add more cases if you have more sending method
enum WatchSendMethod: String {
case sendWakeupToFlutter
case sendCloseToFlutter
}
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
session.activate()
}
func sendDataMessage(for method: WatchSendMethod, data: [String: Any] = [:]) {
sendMessage(for: method.rawValue, data: data)
}
}
extension WatchViewModel: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
func sessionReachabilityDidChange(_ session: WCSession) {
print("iPhone reachability: \(session.isReachable)")
if(session.isReachable) {
//invoke sendWakeupToFlutter via sendMessage when reachability is true
sendDataMessage(for: .sendWakeupToFlutter)
}
}
// Receive message From AppDelegate.swift that send from iOS devices
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
DispatchQueue.main.sync {
guard let method = message["method"] as? String, let enumMethod = WatchReceiveMethod(rawValue: method) else {
return
}
switch enumMethod {
case .sendLoadingStateToNative:
self.loading = (message["data"] as? Bool) ?? false
case .sendSSEDeviceListToNative:
self.deviceList = (message["data"] as? String) ?? ""
let data = self.deviceList.data(using: .utf8)!
do {
self.pubDeviceList = try JSONDecoder().decode([Device].self, from: data)
} catch let error {
print(error)
}
}
}
}
func sendMessage(for method: String, data: [String: Any] = [:]) {
guard session.isReachable else {
print("ios not reachable")
return
}
print("ios is reachable")
let messageData: [String: Any] = ["method": method, "data": data]
let callDepth = 10
session.sendMessage(messageData, replyHandler: nil, errorHandler: nil)
}
}
Does someone know how to fix this? Thanks in advance!
EDIT: The change in my watch extension so far:
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if (activationState == WCSessionActivationState.activated) {
sendDataMessage(for: .sendWakeupToFlutter)
}
}
NOTE: The accepted answer didn't solve my problem entirely, but improved the situation. I ended up making a rather independent Watch App w/o using Flutter MethodChannels.
It probably won't fix your issue but you should be careful about using session.isReachable
- its value is only valid for a session that is activated
, which it almost certainly is (activate session is async but quick I think), but WatchOS has a number of APIs where the value can only be trusted if certain conditions are met, and you should check, otherwise you end up with a value like true or false when the real value should be 'don't know'
IIRC isReachable
is normally true from watch to counterpart iPhone app, you should consider sending the wake up proactively when the session activates.