Search code examples
iosswiftflutterapple-watch

Apple Watch Companion App: sendMessage doesn't work with quit iOS App


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.


Solution

  • 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.