Maintenance Log
Real problems I ran into during development, how I traced the root cause,
and how I resolved them - written up in a maintenance-log format.
Not everything got fixed, and structural limitations are labeled honestly as limitations.
- FINDING
Identifying the problem
Even while mirroring, the iPhone and Watch were each running their own LocationService and RunningCenter independently. Starting from the Watch still caused a delay while the iPhone acquired its own GPS lock.
- ACTION
Changing direction
Extended the existing startOrigin property to also govern whether a device tracks location. Only the leading device turns on GPS and sends the computed results to the other; the mirroring device just receives and displays.
- DISCOVERY
Implementing both directions
sendFlightData() only existed on the iOS side, so data could only flow iPhone → Watch. In a Watch-led mirroring session, there was no path at all for the Watch's computed data to reach the iPhone - a hidden gap.
- RESULT
Closing the gap
Added elapsedTime to the FlightData payload and synced it with a 3-second throttle. All four scenarios - iPhone-only, Watch-only, and bidirectional mirroring - now resolve to a single startOrigin-based flow.
- FINDING
Discovery
Stack trace tracing pointed to a crash inside session(_:activationDidCompleteWith:error:).
- ROOT CAUSE
Root cause
The class itself was declared nonisolated, but without marking the delegate methods inside the extension explicitly, Xcode 26's SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor default re-inferred them as @MainActor.
- FINDING
Recurrence
Even after the first fix, the same crash pattern reappeared - only on real devices - inside didReceiveUserInfo.
- ACTION
Full audit
Audited every WCSessionDelegate callback across both the +iOS and +watchOS extensions and added the missing nonisolated everywhere it was needed.
- APPROACH
First implementation
Used Timer.publish().connect() to control the start point directly, choosing it over autoconnect() with pause support in mind.
- FINDING
Restart failure
The timer wouldn't run again after a restart. connect() is a one-time connection - once a Publisher is cancel()'d, it can't be reconnected.
- ACTION
Switching to autoconnect
Switched to subscribing fresh with autoconnect() + sink every time start() is called, storing it in a Set<AnyCancellable> and clearing it with removeAll() on stop().
- RESULT
Preventing duplicate subscriptions
Found that tapping start() repeatedly stacked up subscriptions, making seconds jump by 2 or 3 at once. Clearing any existing subscription right when start() runs fixed it for good.
- FINDING
Discovery
Instruments' Swift Concurrency profiler showed that a new AsyncStream and a new Task were being spun up on every single location update.
- ACTION
Promoting to a property
Promoted the continuation to an Actor property. The stream is now opened only once, and processLocation() yields directly through the stored continuation.
- FINDING
Sendable conflict
Setting continuation = nil directly inside onTermination threw 'Actor-isolated property can not be mutated from a Sendable closure' - the closure runs on an arbitrary thread, so it can't touch an Actor-protected property directly.
- RESULT
Splitting init from a task
Created an Actor-isolated method (clearContinuation()) and wrapped it in Task { await ... }. Later split this out into startStream(), called from the View's .task, to clean up the flow further.
- FINDING
Identifying the problem
Only the normal flow (tapping a button) handled state cleanup - abnormal exits like tapping the tab bar or turning the Watch crown weren't covered at all, something I only caught through real-device testing.
- FINDING
Choosing the right signal
Tried using an isRunning flag, but on the iPhone during mirroring, start() is never called, so the flag could read false even though a session was still alive. Switched the check to HealthKitService.shared.session != nil instead.
- ACTION
Flag pattern
Button actions now set a flag like didNavigateToTouchdown to true first, and .onDisappear interprets the absence of that flag as an 'abnormal exit.' Applied the same pattern consistently across five Views.
- RESULT
Handling the exception
FlightSummaryView is also reused for browsing the Logbook, so it's distinguished with a selectedFlight == nil condition. On Watch, only WatchSummaryView is set to always clean up unconditionally.
- FINDING
First attempt
Tried treating a session as a zombie if 5 seconds had passed since startDate, but couldn't verify it - force-quitting via the debugger immediately severed the connection, making it impossible to check the logs.
- DISCOVERY
Pinning down the cause
Only after switching to os_log / Console.app and resolving a privacy-masking issue did the real cause come into focus: retrieveRemoteSession was re-detecting a session that was already alive on relaunch.
- ACTION
Flag-based detection
A UserDefaults flag (wasZombieSuspected) detected the condition accurately, but calling .end() on it triggered a side effect - it cascaded into ending the Watch session too.
- FINDING
Exploring alternatives
Tried comparing appLaunchTime instead. Ignoring a detected zombie blocked new mirroring sessions, while calling end() reintroduced the earlier side effect - a genuine dilemma.
- RESULT
Confirming the structural limit
Concluded that HKWorkoutSession is a healthd (system daemon) level resource that app code simply can't fully control. Rolled back all related code and kept only the logging infrastructure.
Concluded that HKWorkoutSession is a healthd (system daemon) level resource that app code can't fully control. os_log/Console.app tracing pinpointed the exact cause, but with no fix possible without side effects, this was documented as a known limitation for v1.0.