Repository navigation
Async Metadata Editability
2026-10-03、Issue #12 の修正を確認した。比較元は main の ca98ef4dd63c3f098bb72f76bf54247e18f0063f。修正コミットは 01375b4。作業前に main を origin/main に rebase し、既存の Issue #12 作業ブランチにも取り込んだ。
修正前の一括編集画面は、全選択曲の canEditEmbeddedMetadata を MainActor 上で同期実行していた。ブックマーク解決は全形式で必要になり、FLAC/WAV/Ogg/Opus の判定ではヘッダーや Ogg ページの読み込みも行う。SwiftUI の .task に入れても、処理を実行するアクターは変わらない。
修正後は MainActor で項目 ID・bookmarkData・fileName の値だけを取り出し、EmbeddedMetadataEditabilityChecker の detached task に渡す。fallback ディレクトリの取得、ブックマーク解決、security-scope の開始・終了、形式検証は worker 内で行う。モデルや ModelContext は worker に渡さない。macOS の .withSecurityScope、iOS の空 options、解決失敗時の管理ファイルへの fallback は維持した。
単曲情報画面と歌詞編集画面も同じ非同期判定を使う。歌詞画面では body の再評価ごとに同期判定を繰り返していた箇所を State と task に変更した。保存前の形式検証と追加音声形式の読み込み前の検証も、同期ファイル読み込みを worker 内で行う。
一括編集画面は確認中の表示を出し、テキスト編集と Apply を待機させる。確認中も Close を操作できる。結果を適用する時点で、タスクのキャンセル、要求 ID、選択した項目 ID を照合し、削除済み・別の ModelContext に属する項目を除外する。閉じた画面では要求 ID を無効化する。
worker のキャンセルは detached task に伝播し、同期処理の前後と各項目の間で確認する。開始済みの OS の同期呼び出しを即座に中断できることは前提にしていない。同期処理が遅れて完了しても結果を返さず、後続項目の判定を始めない。別の画面の判定を、前の画面の worker や共通ロックで待たせない。
保存前の判定中に項目が削除されたり参照先が変わったりした場合は、項目 ID・bookmarkData・fileName・ModelContext を再確認して書き込みを中止する。
編集可否は従来の形式対応判定を維持した。MP3/MP4/AIFF 系は拡張子で判定するため、欠損ファイルでも対応形式であれば編集可能と判定する。実際の書き込み時に失敗を報告する。FLAC/WAV/Ogg/Opus はヘッダー等を検証するため、無効なファイルと欠損ファイルは編集対象にならない。判定結果だけで存在や書き込み権限を保証しない。
同じテスト音声から FLAC/WAV/Ogg/Opus を各 750 件コピーし、別パス・別ブックマーク・別 inode の 3000 ファイルを作成した。macOS 27.0.1、Xcode 27.0、Swift 6.4 で、main の同期処理と修正後の worker をそれぞれ 3 回実行した。MainActor に 1 ms 間隔の heartbeat を置き、その最大遅延と同期処理の実行スレッドを記録した。
| 条件 | 修正前 | 修正後 |
|---|---|---|
| ローカルファイル、MainActor heartbeat 最大遅延 | 884.6–939.2 ms | 1.1–20.2 ms |
| ブックマーク解決の MainThread 実行回数 | 3000 | 0 |
| 形式検証の MainThread 実行回数 | 3000 | 0 |
| MainActor での値スナップショット作成 | 同期走査に含む | 0.106–0.120 ms |
| 検証ごとに 1 ms の遅延を注入、heartbeat 最大遅延 | 6224.5–8503.5 ms | 1.2–19.1 ms |
修正後も worker で各 3000 回の解決と検証を行い、全件を編集可能と判定した。1 ms の遅延は低速なストレージ待機を再現するための明示的な注入であり、実ストレージの測定値ではない。
測定は実コードを抽出した独立プロセスで行った。SwiftData のモデル取得や SwiftUI の描画を含む実アプリ全体の時間、iOS の性能、Instruments のトレースは未測定。並行ビルドによる負荷とスケジューリングの揺れもあるため、スループットの改善や描画フレームの時間上限は主張しない。再現手順、コード、入力・ソースのハッシュ、測定結果を保存した。
| 完了条件 | 確認方法 |
|---|---|
| 3000 件の同期 I/O と MainActor 占有の修正前後比較 | 上記の実ファイルベンチマーク。単体テストでも 3000 個の選択 ID を走査し、解決・検証が MainThread で行われないことを確認 |
| 確認中も画面と Close が応答する | 同期 resolver/probe をブロックして MainActor heartbeat が進む単体テスト。遅い判定中に Close し、別の選択で開き直す対象 UI テスト |
| 閉じた画面・前の選択の遅延結果を適用しない | キャンセル前後と各同期処理の待機中を再現し、遅延成功を CancellationError として破棄、後続入力を開始しないことを確認。UI の要求 ID 照合もレビュー |
| 形式対応、欠損、ブックマークの扱いを維持 | 実ブックマーク優先・不正ブックマーク fallback、各形式の有効/無効/欠損入力、既存の拡張子判定を単体テスト。macOS/iOS ビルドと iPhone/iPad 操作を検証 |
| 判定待ちに削除・参照先変更が発生しても書かない | 歌詞/タグ保存 × 削除/fileName 変更の 4 ケースで、遅延成功後も原本・差し替え先・保存済みメタデータが変わらないことを確認 |
検証結果:
- macOS ビルド、全単体テスト 422 件/67 スイート、対象 UI テスト 1 件が成功。
- iOS Simulator ビルドが成功。iPhone 18 Pro(iOS 27)の対象 UI テストと既存の設定・一括編集操作テストの計 2 件が成功。
- iPad Pro 13-inch(iOS 27)で新規単体テスト 8 メソッドと対象 UI テスト 1 件が成功。初回は成功後に
XCTHRunDestinationAllocator.collectSimulatorDiagnosticsで待機したため、終了済みテストの診断収集を中断し、-collect-test-diagnostics neverで再実行した。再実行は全件成功し、xcodebuild の終了コード 0 を確認した。 - SwiftLint 0.65.1 はエラー 0 件、既存の警告 43 件。ベースラインは移動した既存指摘 2 件の場所だけを更新し、指摘を追加していない。ビュー分割と一部ヘルパーの抽出で、新しく発生したサイズ・複雑度違反は解消した。
- 差分と全呼び出し元をレビューし、移動した既存コードが必要なアクセス修飾子とファイル境界を除いて main と一致することを確認した。
git diff --checkも成功。
画面操作は遅延判定を注入した少数の選択で検証した。3000 件の画面を直接開いて描画やスクロールを計測する操作、実機・外部ストレージでの性能は未検証。音声の本処理・再生・解析全体が MainActor を占有しないことまで、この修正の検証範囲に含めていない。