fix(watch-connectivity): improve WCSession lifecycle and message reliability

This commit is contained in:
2026-08-07 15:32:40 +02:00
parent e64e2353d9
commit 00a8220096
8 changed files with 266 additions and 71 deletions
+52 -18
View File
@@ -3,21 +3,20 @@ import WatchConnectivity
class PhoneConnector: NSObject, ObservableObject, WCSessionDelegate {
static let shared = PhoneConnector()
private var workoutManager: WorkoutManager?
var workoutManager: WorkoutManager? {
didSet {
if let pending = pendingWorkouts {
pending.forEach { workoutManager?.addWorkoutFromPhone($0) }
pendingWorkouts = nil
}
}
}
private var pendingWorkouts: [StoredWorkout]?
private override init() {
super.init()
}
func setup(workoutManager: WorkoutManager) {
self.workoutManager = workoutManager
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
}
// Send completed workout data to phone
static func sendCompletedWorkout(_ laps: [[String: Any]], duration: TimeInterval, workoutId: String) {
let message: [String: Any] = [
@@ -29,7 +28,8 @@ class PhoneConnector: NSObject, ObservableObject, WCSessionDelegate {
if WCSession.default.isReachable {
WCSession.default.sendMessage(message, replyHandler: nil) { error in
print("Failed to send: \(error)")
print("[PhoneConnector] sendMessage failed, falling back to transferUserInfo: \(error)")
WCSession.default.transferUserInfo(message)
}
} else {
WCSession.default.transferUserInfo(message)
@@ -39,32 +39,66 @@ class PhoneConnector: NSObject, ObservableObject, WCSessionDelegate {
// MARK: - WCSessionDelegate
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("WatchConnectivity activated: \(activationState.rawValue)")
print("[PhoneConnector] Activated: state=\(activationState.rawValue), isReachable=\(session.isReachable)")
}
func sessionReachabilityDidChange(_ session: WCSession) {
print("[PhoneConnector] Reachability changed: isReachable=\(session.isReachable)")
}
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
print("[PhoneConnector] didReceiveMessage (no reply)")
DispatchQueue.main.async { [weak self] in
self?.handleMessage(message)
}
}
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
print("[PhoneConnector] didReceiveMessage (with reply)")
replyHandler(["status": "received"])
DispatchQueue.main.async { [weak self] in
self?.handleMessage(message)
}
}
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
print("[PhoneConnector] didReceiveUserInfo")
DispatchQueue.main.async { [weak self] in
self?.handleMessage(userInfo)
}
}
private func handleMessage(_ message: [String: Any]) {
guard let type = message["type"] as? String else { return }
guard let type = message["type"] as? String else {
print("[PhoneConnector] No type in message: \(message.keys)")
return
}
print("[PhoneConnector] Received message type: \(type)")
switch type {
case "workoutPlan":
if let data = try? JSONSerialization.data(withJSONObject: message),
let workout = try? JSONDecoder().decode(StoredWorkout.self, from: data) {
workoutManager?.addWorkoutFromPhone(workout)
guard let data = try? JSONSerialization.data(withJSONObject: message) else {
print("[PhoneConnector] Failed to serialize message")
return
}
print("[PhoneConnector] JSON data: \(String(data: data, encoding: .utf8) ?? "nil")")
do {
let workout = try JSONDecoder().decode(StoredWorkout.self, from: data)
print("[PhoneConnector] Decoded workout: \(workout.name ?? "nil"), sets: \(workout.sets.count)")
if let manager = workoutManager {
manager.addWorkoutFromPhone(workout)
} else {
print("[PhoneConnector] workoutManager not set, queuing workout")
if pendingWorkouts == nil { pendingWorkouts = [] }
pendingWorkouts?.append(workout)
}
} catch {
print("[PhoneConnector] Decode error: \(error)")
}
default:
break
print("[PhoneConnector] Unknown type: \(type)")
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import SwiftUI
struct ContentView: View {
@StateObject private var workoutManager = WorkoutManager()
@EnvironmentObject var workoutManager: WorkoutManager
var body: some View {
NavigationView {
+10 -14
View File
@@ -25,8 +25,6 @@ class WorkoutManager: ObservableObject {
private var lapStartTime: Date?
private var completedLaps: [CompletedLap] = []
private let suiteName = "group.com.swimbuddy.app"
init() {
healthStore = HKHealthStore()
}
@@ -34,7 +32,7 @@ class WorkoutManager: ObservableObject {
// MARK: - Data Persistence
func loadSavedWorkouts() {
guard let defaults = UserDefaults(suiteName: suiteName) else { return }
let defaults = UserDefaults.standard
if let data = defaults.data(forKey: "savedWorkouts"),
let workouts = try? JSONDecoder().decode([StoredWorkout].self, from: data) {
@@ -48,7 +46,7 @@ class WorkoutManager: ObservableObject {
}
func saveWorkoutsToStorage() {
guard let defaults = UserDefaults(suiteName: suiteName) else { return }
let defaults = UserDefaults.standard
if let data = try? JSONEncoder().encode(savedWorkouts) {
defaults.set(data, forKey: "savedWorkouts")
}
@@ -234,16 +232,14 @@ class WorkoutManager: ObservableObject {
"duration": elapsedSeconds,
]
if let defaults = UserDefaults(suiteName: suiteName) {
var completed = completedWorkouts
var summary = selectedWorkout
summary?.completedAt = ISO8601DateFormatter().string(from: Date())
if let s = summary {
completed.insert(s, at: 0)
}
if let data = try? JSONEncoder().encode(completed) {
defaults.set(data, forKey: "completedWorkouts")
}
var completed = completedWorkouts
var summary = selectedWorkout
summary?.completedAt = ISO8601DateFormatter().string(from: Date())
if let s = summary {
completed.insert(s, at: 0)
}
if let data = try? JSONEncoder().encode(completed) {
UserDefaults.standard.set(data, forKey: "completedWorkouts")
}
if WCSession.default.isReachable {
+10 -1
View File
@@ -1,15 +1,24 @@
import SwiftUI
import WatchConnectivity
@main
struct SwimTrackerWatchApp: App {
@StateObject private var workoutManager = WorkoutManager()
init() {
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = PhoneConnector.shared
session.activate()
}
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(workoutManager)
.onAppear {
PhoneConnector.shared.setup(workoutManager: workoutManager)
PhoneConnector.shared.workoutManager = workoutManager
}
}
}