diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5426a8f9..629bbaca 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 77; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -182,6 +182,8 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 7310A7D52EB10962002C0FD3 /* WatchRunner Watch App */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = "WatchRunner Watch App"; sourceTree = ""; }; @@ -669,14 +671,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -734,14 +732,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -792,14 +786,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App-frameworks.sh\"\n"; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 7dcf80a7..63d1fb5f 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -5,14 +5,14 @@ import WatchConnectivity @main @objc class AppDelegate: FlutterAppDelegate { let notifyDelegate = NotifyDelegate() - private var watchConnectivityService: WatchConnectivityService? - + private static var sharedWatchConnectivityService: WatchConnectivityService? + override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { UNUserNotificationCenter.current().delegate = notifyDelegate - + let replyableMessageCategory = UNNotificationCategory( identifier: "CHAT_MESSAGE", actions: [ @@ -25,38 +25,45 @@ import WatchConnectivity intentIdentifiers: [], options: [] ) - UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory]) GeneratedPluginRegistrant.register(with: self) + // Always initialize and retain a strong reference if WCSession.isSupported() { - watchConnectivityService = WatchConnectivityService() + AppDelegate.sharedWatchConnectivityService = WatchConnectivityService.shared + } else { + print("[iOS] WCSession not supported on this device.") } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } -class WatchConnectivityService: NSObject, WCSessionDelegate { - private let session: WCSession - - override init() { - self.session = .default +final class WatchConnectivityService: NSObject, WCSessionDelegate { + static let shared = WatchConnectivityService() + private let session: WCSession = .default + + private override init() { super.init() - print("[iOS] Activating WCSession") - self.session.delegate = self - self.session.activate() + print("[iOS] Activating WCSession...") + session.delegate = self + session.activate() } + // MARK: - WCSessionDelegate + func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { if let error = error { - print("[iOS] WCSession activation failed with error: \(error.localizedDescription)") - return + print("[iOS] WCSession activation failed: \(error.localizedDescription)") + } else { + print("[iOS] WCSession activated with state: \(activationState.rawValue)") + if activationState == .activated { + sendDataToWatch() + } } - print("[iOS] WCSession activated with state: \(activationState.rawValue)") } - + func sessionDidBecomeInactive(_ session: WCSession) {} func sessionDidDeactivate(_ session: WCSession) { @@ -69,10 +76,7 @@ class WatchConnectivityService: NSObject, WCSessionDelegate { let token = UserDefaults.standard.getFlutterToken() let serverUrl = UserDefaults.standard.getServerUrl() - print("[iOS] Retrieved token: \(token ?? "nil")") - print("[iOS] Retrieved serverUrl: \(serverUrl)") - - var data: [String: Any] = ["serverUrl": serverUrl] + var data: [String: Any] = ["serverUrl": serverUrl ?? ""] if let token = token { data["token"] = token } @@ -81,4 +85,25 @@ class WatchConnectivityService: NSObject, WCSessionDelegate { replyHandler(data) } } + + func sendDataToWatch() { + guard session.activationState == .activated else { + return + } + + let token = UserDefaults.standard.getFlutterToken() + let serverUrl = UserDefaults.standard.getServerUrl() + + var data: [String: Any] = ["serverUrl": serverUrl ?? ""] + if let token = token { + data["token"] = token + } + + do { + try session.updateApplicationContext(data) + print("[iOS] Sent application context: \(data)") + } catch { + print("[iOS] Failed to send application context: \(error.localizedDescription)") + } + } } diff --git a/ios/WatchRunner Watch App/State/WatchConnectivityService.swift b/ios/WatchRunner Watch App/State/WatchConnectivityService.swift index 88f18465..56e1c6c0 100644 --- a/ios/WatchRunner Watch App/State/WatchConnectivityService.swift +++ b/ios/WatchRunner Watch App/State/WatchConnectivityService.swift @@ -1,15 +1,6 @@ -// -// WatchConnectivityService.swift -// WatchRunner Watch App -// -// Created by LittleSheep on 2025/10/29. -// - -import Foundation import WatchConnectivity import Combine - -// MARK: - Watch Connectivity +import Foundation class WatchConnectivityService: NSObject, WCSessionDelegate, ObservableObject { @Published var token: String? @@ -49,6 +40,22 @@ class WatchConnectivityService: NSObject, WCSessionDelegate, ObservableObject { } } + func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { + print("[watchOS] Received application context: \(applicationContext)") + DispatchQueue.main.async { + if let token = applicationContext["token"] as? String { + self.token = token + self.userDefaults.set(token, forKey: self.tokenKey) + } + if let serverUrl = applicationContext["serverUrl"] as? String { + self.serverUrl = serverUrl + self.userDefaults.set(serverUrl, forKey: self.serverUrlKey) + } + self.isFetched = true + self.errorMessage = nil + } + } + func session(_ session: WCSession, didReceiveMessage message: [String : Any]) { print("[watchOS] Received message: \(message)") DispatchQueue.main.async { @@ -64,41 +71,43 @@ class WatchConnectivityService: NSObject, WCSessionDelegate, ObservableObject { } func requestDataFromPhone() { - if self.isFetched == true { - print("[watchOS] Skipped fetch from phone due to tried.") - return + // Check if we already have valid data to avoid unnecessary requests + if let token = self.token, let serverUrl = self.serverUrl, !token.isEmpty, !serverUrl.isEmpty { + print("[watchOS] Skipped fetch - already have valid data") + self.isFetched = true + return + } + + guard session.activationState == .activated else { + print("[watchOS] Session not activated yet, state: \(session.activationState.rawValue)") + DispatchQueue.main.async { + self.errorMessage = "Session not ready yet" } - - guard session.isReachable else { + return + } + + print("[watchOS] Requesting data from phone") + session.sendMessage(["request": "data"]) { [weak self] response in + guard let self = self else { return } + print("[watchOS] Received reply: \(response)") + DispatchQueue.main.async { self.isFetched = true - let errorMsg = "Phone is not reachable" - print("[watchOS] \(errorMsg)") - DispatchQueue.main.async { - self.errorMessage = errorMsg + if let token = response["token"] as? String { + self.token = token + self.userDefaults.set(token, forKey: self.tokenKey) } - return + if let serverUrl = response["serverUrl"] as? String { + self.serverUrl = serverUrl + self.userDefaults.set(serverUrl, forKey: self.serverUrlKey) + } + self.errorMessage = nil // Clear any previous errors } - - print("[watchOS] Requesting data from phone") - session.sendMessage(["request": "data"]) { [weak self] response in - guard let self = self else { return } - print("[watchOS] Received reply: \(response)") - DispatchQueue.main.async { - self.isFetched = true - if let token = response["token"] as? String { - self.token = token - self.userDefaults.set(token, forKey: self.tokenKey) - } - if let serverUrl = response["serverUrl"] as? String { - self.serverUrl = serverUrl - self.userDefaults.set(serverUrl, forKey: self.serverUrlKey) - } - } - } errorHandler: { error in - print("[watchOS] sendMessage failed with error: \(error.localizedDescription)") - DispatchQueue.main.async { - self.errorMessage = "Failed to get data from phone: \(error.localizedDescription)" - } + } errorHandler: { error in + print("[watchOS] sendMessage failed with error: \(error.localizedDescription)") + DispatchQueue.main.async { + self.errorMessage = "Failed to get data from phone: \(error.localizedDescription)" + // Don't set isFetched = true on error - allow retry } } } +} diff --git a/pubspec.yaml b/pubspec.yaml index 77ce22f6..49d2398f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 3.3.0+140 +version: 3.3.0+142 environment: sdk: ^3.7.2