Compare commits

...

4 Commits

Author SHA1 Message Date
Roberto Viola
78261c16e4 fixing build 2025-12-09 11:19:56 +01:00
Roberto Viola
a21e328fa2 Merge branch 'master' into claude/fix-ios-live-activities-01AUmHSnuaZ1gWogb1npryRM 2025-12-09 10:52:35 +01:00
Roberto Viola
604a41bbfc Merge branch 'master' into claude/fix-ios-live-activities-01AUmHSnuaZ1gWogb1npryRM 2025-12-09 10:51:41 +01:00
Claude
984d39de68 Fix iOS Live Activities not closing when app is killed
Implemented inactivity timer that auto-closes Live Activity after 10 seconds without updates.

How it works:
- Timer starts when Live Activity is created
- Every update resets the timer back to 10 seconds
- If no updates received for 10 seconds, Live Activity auto-closes
- Timer uses RunLoop.common mode to work in background

This handles scenarios:
✓ App crashes or loses connection → auto-closes after 10 seconds
✓ Bluetooth disconnects → auto-closes after 10 seconds
✓ App in background continues workout → keeps updating, stays open
✓ Normal workout stop → closes immediately via explicit endActivity()

LIMITATION - Force-kill from app switcher:
When user force-kills app from app switcher, iOS terminates the process immediately.
No code can execute, including timers. In this case:
- Live Activity will NOT auto-close (iOS limitation)
- Stale date (15 seconds) will mark it visually as outdated
- User must manually dismiss from Lock Screen/Dynamic Island

This is still a major improvement: handles crashes, disconnections, and normal termination.
Force-kill scenario would require push notifications from a backend server to fix completely.
2025-12-07 19:35:07 +00:00
2 changed files with 65 additions and 10 deletions

View File

@@ -4569,7 +4569,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1221;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = NO;
@@ -4770,7 +4770,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "../src/ios/qdomyos-zwift.entitlements";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1221;
DEBUG_INFORMATION_FORMAT = dwarf;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_TEAM = 6335M7T29D;
@@ -5007,7 +5007,7 @@
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1221;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -5103,7 +5103,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1221;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_BITCODE = YES;
@@ -5195,7 +5195,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1221;
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
ENABLE_PREVIEWS = YES;
@@ -5311,7 +5311,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = "watchkit Extension/WatchKit Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1221;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"watchkit Extension/Preview Content\"";
ENABLE_BITCODE = YES;
@@ -5421,7 +5421,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1221;
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -5512,7 +5512,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_ENTITLEMENTS = QZWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1220;
CURRENT_PROJECT_VERSION = 1221;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6335M7T29D;
ENABLE_NS_ASSERTIONS = NO;

View File

@@ -7,11 +7,46 @@
import Foundation
import ActivityKit
import UIKit
@available(iOS 16.1, *)
@available(iOS 16.2, *)
@objc public class LiveActivityBridge: NSObject {
private var currentActivity: Activity<QZWorkoutAttributes>?
private var inactivityTimer: Timer?
// Timeout in seconds - if no updates received, auto-close the Live Activity
private let inactivityTimeout: TimeInterval = 10.0
// MARK: - Private Methods
private func startInactivityTimer() {
// Cancel existing timer
inactivityTimer?.invalidate()
// Create new timer that fires after inactivityTimeout seconds
// Using RunLoop.common mode ensures timer works even when app is in background
inactivityTimer = Timer.scheduledTimer(
timeInterval: inactivityTimeout,
target: self,
selector: #selector(inactivityTimerFired),
userInfo: nil,
repeats: false
)
RunLoop.current.add(inactivityTimer!, forMode: .common)
}
@objc private func inactivityTimerFired() {
print("⚠️ No Live Activity updates received for \(inactivityTimeout) seconds - auto-closing")
endActivity()
}
private func stopInactivityTimer() {
inactivityTimer?.invalidate()
inactivityTimer = nil
}
// MARK: - Public Methods
@objc public func startActivity(deviceName: String, useMiles: Bool) {
// Check if Live Activities are supported and enabled
@@ -42,6 +77,9 @@ import ActivityKit
)
currentActivity = activity
print("✅ Live Activity started successfully (useMiles: \(useMiles))")
// Start inactivity timer
startInactivityTimer()
} catch {
print("❌ Failed to start Live Activity: \(error.localizedDescription)")
}
@@ -53,6 +91,9 @@ import ActivityKit
return
}
// Reset inactivity timer - we received an update, so activity is still alive
startInactivityTimer()
let updatedState = QZWorkoutAttributes.ContentState(
speed: speed,
cadence: cadence,
@@ -64,16 +105,30 @@ import ActivityKit
)
Task {
await activity.update(using: updatedState)
// Also set stale date as fallback indicator when app is killed
// This won't close the Live Activity, but at least marks it as outdated visually
let staleDate = Date().addingTimeInterval(inactivityTimeout + 5)
await activity.update(
ActivityContent<QZWorkoutAttributes.ContentState>(
state: updatedState,
staleDate: staleDate
)
)
}
}
@objc public func endActivity() {
guard let activity = currentActivity else {
// Stop timer even if no activity
stopInactivityTimer()
return
}
// Stop the inactivity timer
stopInactivityTimer()
Task {
// Use .immediate to dismiss from both Dynamic Island and Lock Screen immediately
await activity.end(using: nil, dismissalPolicy: .immediate)
currentActivity = nil
print("Live Activity ended")