RunWayホームへMaintenance Log

整備記録

開発中に実際に直面した問題と原因追跡、解決までの過程を
整備ログの形式でまとめました。すべてが解決したわけではなく、
構造的な限界は限界として正直に残しています。

5
Resolved
1
Known Limitation
  1. FINDING

    問題の認識

    ミラーリング中でもiPhoneとWatchがそれぞれ独自にLocationServiceとRunningCenterを動かしていた。Watchから開始しても、iPhoneが新たにGPSロックを取得する間に遅延が発生していた。

  2. ACTION

    方向転換

    既存のstartOriginを位置追跡の要否を判断する基準にまで拡張。主導端末のみGPSをオンにして計算結果を相手に送信し、ミラーリング端末は受信後に表示するだけとした。

  3. DISCOVERY

    双方向の実装

    sendFlightData()がiOS側にしか存在せず、iPhone → Watch方向にしか送れなかった。Watch主導のミラーリングでは、Watchが計算したデータをiPhoneに送る経路そのものが存在しないという隠れた盲点だった。

  4. RESULT

    補完

    elapsedTimeをFlightDataのペイロードに乗せ、3秒スロットルで同期。iPhone単独 / Watch単独 / 双方向ミラーリングの4つのシナリオがstartOrigin一つで整理された。

  1. FINDING

    発見

    スタックトレースを追跡した結果、session(_:activationDidCompleteWith:error:)内でクラッシュが発生していた。

  2. ROOT CAUSE

    原因

    クラス自体はnonisolatedで宣言していたが、extension内のdelegateメソッド自体に明示しないと、Xcode 26のSWIFT_DEFAULT_ACTOR_ISOLATION = MainActorのデフォルト設定により再び@MainActorと推論されてしまう。

  3. FINDING

    再発

    1次修正後もdidReceiveUserInfo内で同じパターンのクラッシュが実機でのみ再現した。

  4. ACTION

    全数点検

    +iOS、+watchOS extensionのWCSessionDelegateコールバックを全て点検し、漏れていたnonisolatedをすべて追加した。

  1. APPROACH

    1次実装

    Timer.publish().connect()で開始タイミングを直接制御。一時停止の実装を見据えてautoconnect()の代わりに選択した。

  2. FINDING

    再起動の失敗

    停止後に再開してもタイマーが動かない。connect()は一回限りの接続で、一度cancel()したPublisherは再接続できない。

  3. ACTION

    autoconnectへの転換

    start()が呼ばれるたびにautoconnect() + sinkで毎回新しく購読。Set<AnyCancellable>に保存し、stop()時にremoveAll()で整理した。

  4. RESULT

    重複購読の防止

    start()を連続で押すと購読が積み重なり、秒数が2、3ずつ飛ぶ問題も発見。start()の実行時点で既存の購読を先に整理するよう補完し、最終的に解決した。

  1. FINDING

    発見

    InstrumentsのSwift Concurrencyプロファイラーで確認したところ、位置情報の更新のたびに新しいAsyncStreamと新しいTaskが繰り返し生成されていた。

  2. ACTION

    プロパティへの昇格

    continuationをActorのプロパティに昇格。ストリームは一度だけ開いておき、processLocation()内部で保存されたcontinuationから直接yield()するように変更した。

  3. FINDING

    Sendableの衝突

    onTerminationでcontinuation = nilを直接呼び出すと「Actor-isolated property can not be mutated from a Sendable closure」エラー。任意のスレッドで実行される@Sendableクロージャのため、Actor保護されたプロパティに直接アクセスできない。

  4. RESULT

    init → taskの分離

    Actor-isolatedなメソッド(clearContinuation())を作り、Task { await ... }で包んで解決。その後startStream()として分離し、Viewの.taskから呼び出すよう再整理した。

  1. FINDING

    問題の認識

    正常なフロー(ボタンクリック)だけが状態整理を処理しており、異常な離脱(タブバーのクリック、Watchのクラウン操作)は全くカバーされていないことを実機テストで発見した。

  2. FINDING

    判断基準

    isRunningフラグで判断しようとしたが、ミラーリング中のiPhoneはstart()を呼ばないため、セッションが生きていてもfalseになる場合があった。HealthKitService.shared.session != nilを基準に置き換えた。

  3. ACTION

    フラグパターン

    ボタンアクションでdidNavigateToTouchdownのようなフラグを先にtrueに設定し、.onDisappearはそのフラグが立っていないことを「異常離脱」と解釈するパターンを5つのViewに同様に適用した。

  4. RESULT

    例外処理

    FlightSummaryViewはLogbook閲覧時にも使われるViewのため、selectedFlight == nil条件で区別。Watchは WatchSummaryViewのみ例外的に無条件で整理するようにした。

  1. FINDING

    1次試行

    startDate基準で5秒経過したらゾンビとみなす方式を試みたが検証不可 - デバッガーで強制終了した直後に接続が切れ、ログを確認できなかった。

  2. DISCOVERY

    原因の特定

    os_log / Console.appに切り替えてprivacyマスキングの問題を解決した後、ようやく本当の原因を特定した。retrieveRemoteSessionが再起動時に既に生きているセッションを再検知する構造だった。

  3. ACTION

    フラグベースの判別

    UserDefaultsフラグ(wasZombieSuspected)による判別は正確だったが、.end()呼び出し時にWatchセッションまで連鎖終了する副作用が発生した。

  4. FINDING

    代替案の模索

    appLaunchTime比較方式へ転換。ゾンビと判断しても無視すると新しいミラーリングがブロックされ、end()を呼ぶと先の副作用が再発するというジレンマだった。

  5. RESULT

    構造的限界の確定

    HKWorkoutSessionはhealthd(システムデーモン)レベルのリソースであり、アプリコードでは完全な制御が不可能だと結論。関連コードを全てロールバックし、ロギングインフラのみ維持した。

FINAL VERDICT

HKWorkoutSessionはhealthd(システムデーモン)レベルのリソースであり、アプリコードでは完全な制御が不可能だと結論。os_log/Console.appベースで原因を正確に特定したが、副作用なしでの解決が不可能だったためv1.0ではknown limitationとして明記した。