iPhone Duo Implementation Plan - log5r/simple-media-player GitHub Wiki

iPhone Duo 対応の実装計画

iPhone Duo を開いた全画面では、macOS 版と同じ「サイドバー、一覧、歌詞/情報、シークバー、LED と再生操作」の構成を目標にする。閉じた画面では既存のタブ、再生ドック、デッキを使う。表示の切り替え中も、再生、ライブラリの選択、編集、書き出しを継続できるようにする。

以下の仕様・工程は 2026 年 10 月 3 日の調査に基づく実装前の計画。アプリの調査対象は 3a8d74d。配置と工程は本アプリ向けの設計案であり、Apple が指定するアプリ構成ではない。実装後の実測値、検証結果と未検証条件は、末尾の「実装記録(2026 年 10 月 4 日)」に追記した。

Apple の仕様と設計への影響

確認した仕様 本アプリへの影響 根拠
内側は 7.6 インチ・1878×2670 pixels、外側は 5.4 インチ・1398×2034 pixels 物理解像度から SwiftUI の幅を計算しない。実際のビューの pt 寸法を使う Apple 技術仕様
Duo の端末種別は開いても .phone 現在の端末種別による大画面/小画面の分岐を改める Apple Frameworks Engineer の回答
内側の全画面は縦横 regular。外側の縦表示は横 compact・縦 regular、横表示は両方 compact size class と局所的な利用可能寸法を併用する Prepare your app for iPhone Duo、2:46
iOS 27.1 SDK でビルドすると全面表示と縦方向の標準バーが有効になる。内側では対応方向の指定に依存できず、safe area は左右非対称になり得る Xcode 27.1 を使用し、回転の名前よりビューの寸法を参照する。前景は各辺の safe area に従う Prepare your app for iPhone Duo、0:30、3:57、6:06
内側/外側の画面を直接判別する API は意図的に提供されない 機種名、画面 ID、特定の inset 数値から内外を推定しない Apple Frameworks Engineer の回答
半折りのヒンジは division region、カメラは occlusion region。ArrangementView と reserved regions は iOS 27.1 のレイアウト手段 独自デッキの重要な表示・操作を折り目やカメラから避ける Strike a pose with adaptive layouts on iPhone Duo
標準バーは縦方向に適応する。独自 toolbar item は縦配置への適合を確認する必要がある iOS の toolbar、TabView、ドックを標準コンテナに任せ、独自アイテムを点検する Raise the bar with iPhone Duo
Split View、動画とアプリを上下に置く表示、複数 scene を利用できる 開いた画面でも狭い/低い表示を扱い、scene ごとの選択と既存の再生モデルの所有を区別する Leverage multiple displays and scenes on iPhone Duo
単一の全画面 scene では開閉だけで scenePhase が変わらない。複数窓では閉じると副 scene が background へ移る場合がある scenePhase を開閉の検出に使わない。通常の lifecycle とレイアウト変更を別々に扱う Apple DTS Engineer の回答

新 API の採用候補は GeometryProxy.reservedRegions、ArrangementView、toolbarVerticalEdge。ヒンジ角を操作や効果に用いる onHingeChange は、今回のレイアウト判定には使わない。Apple もレイアウトには arrangement と region API を案内している。Leverage multiple displays and scenes、2:35

ArrangementView、GeometryProxy.reservedRegions、toolbarVerticalEdge は iOS 27.1 以降である。iOS 側に使用箇所を限定し、実行時の availability を確認する。macOS の既存構成と iOS/iPadOS 27.0 の fallback を保つ。Xcode 27.1 beta は iOS 27.1 SDK を含むが、macOS/iPadOS SDK の版は同一とは限らないため、他 OS に新 API を無条件で導入しない。ArrangementView、reservedRegions、toolbarVerticalEdge、Xcode 27.1 Beta Release Notes

現行実装で変更が必要な箇所

箇所 現状 変更する理由
PlayerLibraryView.swift、48・169 行 .phone なら常に専用 UI。iPad と macOS は desktopView を共有 Duo 内側でも小画面 UI になる
desktopView、178 行 NavigationSplitView と DetailAreaView、下に SeekBarView と BottomPanelView 開いた時の構成として再利用できる
IPhoneLibraryView.swift、4・54 行 usesPhoneLayout が小画面、操作領域、シート寸法、設定表示を兼用。タブ・検索・経路は子ビューの State 大画面の iPhone でもタッチと iOS の操作を保ち、切り替えで検索等を失わないようにする
DetailAreaView.swift、97 行 歌詞/情報は幅 280–320 pt。EQ は別ビューで高さ 200 pt 一覧が狭くなる場合や高さ不足を扱う
LEDHalfBottomPanelView.swift、23 行 左右 50:50、高さ 170 pt 固定。再生列幅が全幅に比例 操作部と LED の必要幅に応じて配分する
BottomPanelView.swift、PitchSpeedControlsView、SeekBarView 音量増減の幅 24 pt、調整列の高さ 34 pt、シークバーの高さ 30 pt 表示の見た目を保ちながらタッチ領域を確保する
DetailAreaView.swift、166 行 書き出しボタンは macOS 限定 広い iOS の UI に共有シートへの入口が必要
IPhoneTrackListView、IPhoneDeckView 選択、ソート、編集提示、デッキのページ等を子ビューが所有 構成の交換で操作途中の状態を破棄しない

広い iOS の曲一覧は、既存の横・縦スクロール対応の一覧を使う。macOS の SwiftUI Table と AppKit の入力ブリッジや、Finder の保存画面を iOS に移植する必要はない。LED、VU、ビジュアライザー、配色、歌詞/情報、EQ、編集・書き出しサービスを共有し、OS 固有の入出力と表示だけを分ける。

画面構成の設計案

開いた全画面の横長配置を最優先とする。macOS 版のパネルの関係と LED の意匠を再現し、タッチ領域と文字の可読性は iOS に合わせる。

┌──────────┬──────────────────────────┬────────────┐
│ ライブラリ │ 曲一覧/アルバム/動画       │ 歌詞・情報   │
│ プレイリスト│ EQ(表示する場合)          │            │
├──────────┴──────────────────────────┴────────────┤
│ シークバー                                        │
├───────────────────────┬──────────────────────────┤
│ 再生・調整・音量・VU     │ LED・曲名・時刻・解析・可視化 │
└───────────────────────┴──────────────────────────┘

LED の左右、classic/LED half は既存の設定に従う。図は LED を右に置いた場合の構成であり、固定寸法や最終デザインを示すものではない。

利用可能な領域 配置
十分な幅と高さの全画面 上の構成。サイドバー、曲一覧、右パネル、下部デッキを同時表示
縦長、文字拡大、幅がやや不足 右パネルを呼び出し式にし、さらに不足する場合はサイドバーを overlay/折りたたみ表示へ。下部操作は段組を変更
compact 幅、狭い Split View 既存の 4 タブ+再生ドック+デッキ。情報、EQ、編集はシートやデッキから到達
高さ不足、動画との上下分割 一覧/動画の領域を優先し、EQ と補助情報を呼び出し式に。再生操作とシークは維持
半折りの横向き設置 上の領域に動画や LED、下の領域に操作。重要な文字、時刻、シーク、ボタンが division region を跨がないよう配置
本のような半折り 標準ナビゲーションの適応を使い、独自デッキの操作群を利用可能な領域内に配置

一時的なパネルの非表示は、ユーザーの保存済み表示設定を書き換えない。領域が広がれば元の設定に従って戻す。regular 幅を大画面 UI の候補とし、各パネルの必要寸法と Dynamic Type を照合する。分割表示の閾値とデッキの配分は、工程 1 の計測後に決める。

実装の順序と各工程の完了条件

1 Xcode 27.1 で寸法と標準コンテナを確認する

Xcode 27.1 と Duo Simulator を用意し、現行アプリを iOS 27.1 SDK でビルドする。Debug の計測で局所ビューの pt 寸法、両 size class、各辺の safe area、縦バーの edge、division/occlusion region と active 状態を取得する。開閉、回転、Split View の左右、動画との上下分割、半折り、キーボード表示を記録する。内部モデルやライブラリは変更しない。

生成 Info.plist の UILaunchScreen、方向指定、scene 設定、UIRequiresFullScreen 等と実表示を確認する。全面表示のために未確認の独自キーを追加しない。UIRequiresFullScreen で開閉時のサイズ変更を防げるとは想定しない。

完了条件は、各表示でアプリが全面を使い、実測値とスクリーンショットからパネル配置の最小幅・高さを決められること。iPad の 27.0 と macOS のビルドも確認し、新 API を使う範囲と fallback を確定する。

2 開閉で保持する操作状態と提示元をそろえる

MainView の分岐の外に、ライブラリ区分/プレイリスト ID、分類内のアルバム/アーティスト/ジャンル、検索、フィルター、ソート、選択曲 ID、複数選択、一覧位置、デッキページを保持する表示状態を置く。電話のタブ・分類内へのナビゲーション経路と、広い画面のサイドバー・アルバム等の選択を同じ意味の状態へ対応付ける。必要な State だけを共有化し、既存のモデル所有を保つ。

情報・歌詞・一括編集、設定、調整コピー、共有書き出し等の提示状態を、交換されない上位ビューへ移す。Deck の fullScreenCover 上の編集シート、調整シートを閉じてからの保存シート、名前入力後の共有シートという提示階層と順序は保つ。すべてのシートを単純に同じ提示元へ集めず、各提示元の identity を安定させる。シートを同じ identity のまま維持できる場合は、草稿をエディター内に残す。エディター自体を交換する場合に限り草稿を安定した編集セッションへ移す。SaveTransformedCopyView.onDisappear によるキャンセルが開閉で発生しないことを確認する。

共有シートの dismantleUIViewController と completion は現在どちらも一時出力を削除する。開閉による controller の再生成と共有の終了を区別し、必要なら共有セッションが一時ファイルを所有する構成へ改める。システムが Files/共有画面を終了する場合も要求と生成済み出力を保持し、重複実行せず再開できるようにする。名前入力から共有への onDismiss が二重実行されないことも確認する。IPhoneShareSheet.swift、43 行

デッキを閉じた後に開く保存処理や AAC 結果についても、提示状態と要求元の項目 ID を保持する。isPhoneDeckPresented によるアラート抑止が、広い画面への移行後も残らないようにする。開閉と、ユーザーによる停止・一時停止・キャンセル・削除は別の操作として扱う。

完了条件は、遅延処理中に compact と expanded を往復しても、編集草稿、対象 ID、検索、選択、保留操作が保たれること。完了後の結果が開始した項目へ一度だけ適用され、削除済み項目や新しい読み込み世代へ適用されないことを確認する。

3 レイアウトとタッチと OS の条件を分ける

.phone の構成分岐を、size class と局所的な利用可能寸法に基づく判定へ置き換える。既存の usesPhoneLayout の利用箇所を、小画面の構成、タッチ操作、シートの利用可能寸法、表示している機能、OS 固有の入出力へ整理する。AppMenuCommands の端末種別による制限も、iOS でのキーボード操作を含めて確認する。

広い画面では既存の NavigationSplitView と共有パネルを使用する。曲一覧のタップ再生、グループ行の操作、複数選択をタッチで使えるようそろえる。広い iOS に「書き出す…」を設け、既存の共有シート処理へ接続する。一覧の列や LED 配置の設定は、それを使用する画面で操作できるようにする。

完了条件は、同じ iPhone の compact/regular で意図した構成が選ばれ、全機能への導線が存在すること。端末種別が .phone のままでも、expanded 表示の操作領域と iOS の書き出しが使えること。

4 下部デッキと補助パネルを利用可能寸法に合わせる

BottomPanelMetrics に iOS 用の必要寸法を集約する。LED、再生、調整、音量、VU の最小寸法から幅を配分し、50:50 の固定配分と全幅比例の再生列幅を iOS の adaptive 表示で置き換える。macOS の既存寸法は既定値として保つ。単純な scaleEffect による全体縮小は採用せず、段組、余白、情報の呼び出し式表示で調整する。

再生・調整・音量・シーク等の操作領域は本アプリの目標として 44×44 pt 以上を確保し、隣の操作と重ならないようにする。シーク中に画面や曲が変わった場合は、ドラッグ開始時の項目と世代を照合して終了処理を適用する。画面変更だけで再生エンジンや解析を再初期化しない。

右の歌詞/情報と EQ は、利用可能な幅・高さに応じて常設から呼び出し式へ移す。列の省略やパネルの縮退は表示上の判断にとどめ、設定を上書きしない。日英のアクセシビリティ文言をそろえ、LED の EQ/KEY/SPEED/VOLUME/BPM 等は README の表記方針を維持する。

完了条件は、工程 1 で計測した横長の全画面・通常文字サイズ・十分な利用可能幅と高さで、macOS と同じパネル関係が成立すること。縦長、小さい領域、最大文字では前述の縮退を適用し、再生・シーク・音量等に到達でき、重なり、文字切れ、画面外の操作がないこと。

5 半折りと縦方向の標準バーに適応する

iOS 27.1 の reserved regions を局所座標で参照し、独自デッキの操作群や動画/LEDを予約領域を避けた場所へ配置する。必要な箇所だけ ArrangementView を採用する。NavigationSplitView 全体を ArrangementView に包まず、ナビゲーションの詳細領域内の表示部と操作部などに用いる。連続スクロールの一覧や歌詞を不用意に別領域へ移動させない。Apple の配置とコンテナの説明

標準 toolbar、TabView、sheet はシステムの適応に任せる。toolbarVerticalEdge は実際のバーの表示状態ではなく、その context でシステムが選ぶ edge を示す。独自 toolbar item は適合するものだけ .axisBehavior(.verticalPreferred) を候補とし、横配置が必要なものは保持する。compact ドックは既存の標準アクセサリーを使い、バーが縦になる場合の寸法とキーボードとの関係を点検する。toolbarVerticalEdge の契約、縦方向に適応する toolbar item

完了条件は、半折り・左右の Split View・内側カメラによる occlusion の各条件で、重要な表示と操作が妨げられないこと。API の未提供 OS や region のない表示でも通常の adaptive 構成が使えること。

6 検証と文書を仕上げる

新規のレイアウト判定・状態対応・シークの対象判定に単体テストを設ける。既存 iPhone UI テストの .phone だけによる対象判定を見直し、compact の起動状態を明示する。Duo の expanded と開閉移行の UI テストを別に追加する。

macOS/汎用 iOS Simulator をビルドし、PlayerPlaybackStateTests、LazyLyricsLoadingTests、EditableMetadataDraftCancellationTests、TransformedExportCancellationTests、MusicAnalysisTests と新規テスト、./scripts/lint.sh を実行する。UI テストは下表の重要な操作と移行に絞る。現在の CI は macOS のビルド・単体テスト、iOS のビルド、lint を実行しているため、Duo 用のジョブは工程 1 で runtime と開閉の自動化手段が確認できてから Xcode 27.1 を明示して追加する。自動化できない条件は対象を絞った実操作と記録で補い、geometry の模擬テストだけで実際の開閉を検証済みとしない。

日英の docs/iphone-layout と README に対応範囲・配置・必要 SDK を反映し、この Wiki に実測値、判断した閾値、検証結果と未検証条件を追記する。SwiftLint の新しい違反を baseline へ追加しない。

操作別の完了条件と確認方法

操作・状態 完了条件 確認方法
macOS に近い表示 開いた横長の全画面でサイドバー・一覧・右情報・シーク・下部 LED/操作が使える macOS と Duo の同じ曲・設定の画像を比較。パネル矩形、文字とタップ領域も確認
音声/動画の再生 開閉で曲、キュー、再生/一時停止/停止状態、EQ、音量が保たれ、再生中は時刻と出力が継続する 再生中・一時停止中・停止中に往復。エンジン出力と時刻の進行を検査し、可聴音源で実機の音切れを確認。動画、調整再生、AirPlay も確認
シーク 開閉後のドラッグ完了が別の曲を動かさない ドラッグ→開閉→曲切替→終了の順序を制御し、対象 ID と位置を検査
取り込み 開閉・分割表示で fileImporter と進捗・エラーが維持され、重複処理されない ファイル選択中とコピー中に開閉。複数音声/動画、エラーを含む fixture と Files provider を使う
解析・情報 開閉で同じ項目の結果を表示し、前の項目の遅延結果を表示しない 遅延解析/情報読み込み中に開閉・項目切替・削除を挟み、完了順を入れ替える
単体/一括メタデータ・歌詞編集 草稿と対象が保たれ、保存後の再読み込みで変更と意図した空値が残る シートとキーボード表示中に開閉、保存/キャンセル/対象削除、再表示を検査
調整コピー・AAC・共有書き出し 開閉で処理をキャンセルせず、結果を一度だけ提示する。共有画面が終了した場合も生成済み出力を失わず再開できる 既存の遅延 AAC fixture を再利用。名前入力、変換、共有中に開閉し、controller の解体/再提示と出力の寿命を検査
プレイリスト・削除 選択、並べ替え、複数選択が維持され、削除後の保留操作が項目を復元しない プレイリスト内で往復、選択中/再生中/処理中の項目を削除し、遅延完了も確認
分類内の閲覧 同じアルバム/アーティスト/ジャンルと一覧位置を開閉後も表示する 各分類からグループ内部へ移動して開閉し、同じ対象と経路を往復で検査
設定・アクセシビリティ wide の設定が使え、compact へ戻っても保存値を保持。重要操作は 44 pt 以上 日英、明暗、LED スタイル、通常/最大文字、VoiceOver、Reduce Motion/Transparency を確認
半折り・分割表示 左右非対称 safe area、division/occlusion、キーボードで前景が切れない Device Hub で各 pose と左右分割を操作し、画像と操作領域を保存。実機でも操作
既存プラットフォーム 通常 iPhone、iPad の全画面/狭い窓、macOS の最小窓で主要操作が使える 既存 IPhoneLayout/LibraryWorkflow/InformationAccessibility、IPadLayout、関連 macOS UI テストを対象指定で実行

既存 PhoneLayoutUITestFixture には無音の音声 3 曲、歌詞、プレイリスト、遅延 AAC がある。可聴音源、動画、fileImporter、実ファイル保存後の再読み込み、ヒンジ/カメラ region の確認は追加データまたは実操作を用意する。大量ライブラリでの開閉中には、リストの再構築や同期 I/O を MainActor で追加していないことを確認し、Instruments の hitches/main-thread 計測で応答性を確認する。既存の解析・デコーダーはレイアウト変更で作り直さない。

調査時点で残っていた確認

今回の環境は macOS 27.0.1、Xcode 27.0(27A266a)。Duo の Xcode 27.1 Simulator による pt 寸法、バーの配置、reserved regions はまだ計測していない。sandbox 内の simctl は CoreSimulatorService に接続できなかった。今回ビルド・単体テスト・UI テスト・lint は実行していない。

reservedRegions の公開 API 文書と Tech Talk には、既定で返る region の active/inactive の説明に差がある。実装では API の引数・返る active 状態を明示して扱い、工程 1 で平ら/半折り/カメラ使用時の入力例を確認する。物理解像度、App Store のスクリーンショット寸法、ビューの pt 寸法は区別し、未確認の scale や inset を固定値として使わない。

tabViewBottomAccessory が Duo の縦バーにどう対応するかは、調査した公式資料では明示を確認できていない。既存ドックの位置・placement・キーボードとの関係を工程 1 で実測し、適応方法を決める。

複数 scene は既存の対応範囲を先に確認する。表示状態は scene ごとに所有し、App が所有する再生モデルの共有関係は維持する。新規の複数プレイヤー、ヒンジ角による再生操作、外側画面への追加 UI は今回の計画に含めない。

実装記録(2026 年 10 月 4 日)

この節は、上記の計画に沿って行った実装と検証を記録する。調査時点の基準は 3a8d74d7778fc2dd5440b095763a9f3313133b96。アプリの変更は作業ツリーに反映し、公開リリースや実機での検証は行っていない。

実装した表示と状態管理

MainView の構成を端末の .phone 判定から horizontal size class と局所寸法による判定へ変更した。compact は既存のタブ・再生ドック・デッキ、regular はサイドバー・一覧・歌詞/情報・シークバー・下部パネルを使う。iOS のサイドバーは基準 200 pt。一覧と詳細の領域に幅 620 pt・高さ 300 pt 以上があるときは幅 240 pt の詳細を横に置く。最大文字サイズや狭い領域では詳細をシートに移し、高さ 430 pt 未満では EQ も呼び出し式にする。保存済みの表示設定は書き換えない。

下部の iOS パネルは、LED と操作群の必要幅から配分と段組を決める。再生・調整・音量・シークの操作領域を 44 pt 以上にし、空間が足りない場合はスクロールで操作を残す。iOS 27.1 の active な division/occlusion と margins を局所座標で避ける。デッキの Display ページに置く ArrangementView は NavigationStack の内側、各 ScrollView の外側に限定した。標準バーには適合する toolbar group の縦配置優先を指定した。iOS 27.0 の fallback と macOS の従来の寸法・ネイティブ操作を保つ。

レイアウト分岐の上に LibraryBrowsingState を保持し、分類、分類内のグループ、プレイリストの経路、検索・フィルター・ソート、選択、複数編集、可視項目のスクロール位置を共有する。情報・歌詞・複数編集・設定・書き出し名・inline アートワーク拡大は安定した親から提示する。アートワークや編集の対象がライブラリから削除された場合は保留対象も解除する。アルバム内の再生順を compact/expanded で共通化し、分類内の書き出しも現在表示しているグループだけを対象にした。

二軸スクロールの一覧は初期位置と項目復元を .topLeading にした。.top で幅の広い行の横中央まで合わせると、先頭の曲名列が表示領域の左へ隠れるためである。

PlayerViewModel は開閉で作り直さない。シークは開始時の項目 ID と再生世代を照合し、停止・項目切り替え・同じ曲の再読み込み・失敗後の古い完了を拒否する。動画の読み込み完了にも世代を照合する。音声エンジンのリセットは動画の読み込みを無効にしない。後者は修正前の失敗と修正後の成功を単体テストで確認した。

共有の一時ファイルは SharedExportSession が所有する。activity controller の解体と共有完了を分け、解体時にはファイルを保持する。再提示後の古い completion は提示 ID で拒否する。共有画面がシステムによって閉じられた場合は、もう一度 Export を選ぶと既存の出力を共有できる。名前入力は iOS の標準 NavigationStack とツールバーへ移し、キーボード表示中も Cancel/Export に到達できるようにした。

README と iPhone の説明を日英で更新し、docs/iphone-duo.md と docs/iphone-duo.ja.md に配置・状態所有・再現手順をまとめた。CI は各コマンドの DEVELOPER_DIR を使って iOS 27.1 以降の SDK を持つ Xcode を選ぶ。端末名や未確認の Info.plist キーは追加していない。SwiftLint の baseline は変更していない。

実測した Duo の表示

Xcode 27.1 Beta(27A9269)、iOS 27.1 Simulator、macOS 27.0.1 を使用した。下表は物理解像度ではなく MainView の content 領域の pt 寸法である。標準バーと safe area によって、シーン全体の寸法より小さくなる。

実際の姿勢・表示 content 領域 size class 観測
Open・横長 867 × 635 pt regular/regular サイドバー・一覧・右詳細を同時表示。下にシークと LED/操作。bottom inset 34 pt、trailing inset 84 pt
Open・縦長 669 × 835 pt regular/regular 詳細は呼び出し式。補助パネルを縮退。top inset 82 pt、bottom inset 34 pt
Closed・縦長 382 × 644 pt compact/regular タブ・再生ドックへ切り替え。division は空。縦バーによる trailing inset 84 pt
Closed・横長 594 × 432 pt compact/compact compact を維持。標準タブバーは縦配置
Book・横長 867 × 635 pt regular/regular active division は x 455.5 pt・幅 40 pt・高さ 669 pt、左右 margins 各 20 pt

これらの値は観測例であり、レイアウト条件に埋め込んでいない。active 状態は .includeInactive で取得した region から明示的に絞る。

検証結果

検証 結果と範囲
iOS 27.1 SDK/macOS ビルド 成功。Duo Simulator 向けと通常 iPhone/iPad の iOS 27.0 runtime 向けもコンパイル可能。macOS は Xcode 27.1 Beta と 27.0 の両方でビルド・リンク成功
最終の関連単体テスト 48 test definitions・parameter を含む 60 実行が成功。LibraryLayoutPolicy、LibraryBrowsingState、AdaptiveBottomPanel、PlayerPlaybackGeneration、SharedExportSession、PlayerPlaybackState、Localization
既存の関連回帰テスト PlayerViewModel、ライブラリ projection/検索・ソート、遅延歌詞、設定、列表示などの対象試験が成功。MusicAnalysis と EditableMetadataDraftCancellation も成功
変換・キャンセル TransformedExportCancellationTests は -parallel-testing-enabled NO で 10 definitions・14 実行が成功。既定の並列では renderer の 5 秒 gate が期限切れになる。無変更の基準 HEAD でも同じ並列失敗を再現し、基準と現行の逐次実行は両方成功した。今回の UI 差分を必要原因としない既存の試験条件の問題
通常 iPhone・iOS 27.0 UI 7 件成功。日英のデッキ、キーボード表示中の日英の書き出し名、共有、設定と複数編集、デッキを閉じた後の遅延 AAC 結果
iPad・iPadOS 27.0 標準レイアウト・設定と最大アクセシビリティ文字での UI 2 件、実際の UIActivityViewController 解体/再生成による出力保持の単体 1 件が成功
Duo の通常操作 expanded 設定と、検索で絞った compact プレイリスト内の曲移動の UI 試験が成功。実画面で 3 列と先頭の曲名列・再生 LED を確認
Duo の実開閉・音声再生 Open→Closed→Book→Open を Device Hub で操作。DEBUG の可聴 fixture を一度だけ再生し、毎秒の状態を記録。同一項目 ID、3 曲の queue、generation 1、pitch 0、rate 1、volume 0.75、playing true を保持。Closed 61.4 秒→Book 90.2 秒→Open 114.2 秒へ進行し、左右 RMS は各姿勢で約 0.574〜0.575。実際の compact/regular と division の active 状態の変化も同じログで確認。Track 1 の 174 サンプルでは同一対象・設定を保持し、時刻は単調増加した
Duo の実回転・音声再生 Device Hub の実回転で 669 × 835 pt→867 × 635 pt の変化を確認。同一 Track 2・generation 2 と正の左右 RMS を保持。Track 1→2 の自然曲送りは 180 秒の終端で回転前に発生したもので、回転による再読み込みではない
lint・差分 scripts/lint.sh --quiet と git diff --check 成功。既存 warning は残るが、新しい UI ファイルの違反や baseline 追加はない

開閉の音声確認は、自動 UI runner の入力制約を避けるため、DEBUG 専用の --ui-testing-duo-autoplay-probe を使った。--ui-testing-phone-layout と併用した場合だけ fixture の先頭曲を一度再生し、毎秒 JSON を stdout に出す。--ui-testing-audible-layout と --ui-testing-duo-long-playback で可聴音声 180 秒を生成する。通常の起動と Release ビルドには影響しない。これは実際の開閉による再生状態と出力の確認であり、UI からの再生開始操作を確認する試験とは区別する。

残る検証条件とテスト環境の制約

Xcode 27.1 Beta の Duo Simulator は、XCUIDevice.orientation が成功を通知しても実表示が回転しない場合があった。実表示と accessibility 座標の不一致を避け、interactive モードではこの代入を行わず Device Hub の実回転を待つ。標準 toolbar の overflow menu は元の accessibility identifier を保持しないため、UI 試験では native menu の label も使う。

Device Hub の CUA 操作は UI runner の待機中に noWindowsAvailable や古い element ID のエラーになる場合があり、折り姿勢の UI 試験は成功としていない。Duo の実キーボード入力中の歌詞・書き出し名草稿の開閉、分類内の開閉と閲覧位置、動画・全画面表示、Files provider 選択中、変換中、一時停止中・停止中の実開閉は、追加の実操作確認が必要である。状態対応・対象削除・共有 controller の再生成・古いシーク対象の拒否は単体試験で確認した範囲とする。

macOS の対象 UI 2 件は、Xcode 27.1 Beta と 27.0 の両方で runner が接続前に約 332〜336 秒で終了し、TestCase 自体を開始できなかった。OS 権限や署名設定の変更は行っていない。macOS のビルド・単体テスト成功を UI 操作の成功とは扱わない。

Duo 実機での可聴音切れ、AirPlay、左右の Split View・動画との上下分割、内側カメラ使用中の occlusion、VoiceOver/Reduce Motion/Transparency、Instruments の MainActor・hitches 計測は未実施。予約領域の矩形計算は単体試験を通したが、これらの実表示条件の代わりにはならない。

最終の Open 横長画像は作業環境の .build/DuoValidation/iphone-duo-open-landscape.png、実姿勢と再生の診断ログは duo-probe-closed.log/duo-probe-book.log/duo-probe-open-restored.log に保存した。再現のコマンドと追加の確認方法は日英の Duo ドキュメントを参照する。

展開した再生画面のデザイン変更(2026 年 10 月 4 日)

この節は 8baed34(iPhone duo 対応)を基準にした追加変更の記録である。上の初期実装の結果とは分けて記録する。アプリの変更は作業ツリーに反映した。

配置と適用範囲

横向きの再生パネルは、安全な領域の右 50% を LED、左 50% を操作部に割り当てる。左側の音量を幅 188 pt 以下・高さ 44 pt の横一列にし、再生・調整ボタンとメーターの余白を詰めた。幅が不足する場合は操作列を増やし、スクロールで操作を残す。

縦向きは再生中心の画面にする。画面上半分を幅いっぱいの上下 2 段に等分し、上段に LED、下段にメーターとランプを配置する。画面下半分はシーク・再生・音量・調整・画面移動の操作部である。これは Open と Book の両方に適用する。Book では active な横方向の division と margins を境界にし、表示部をヒンジより上、操作部を下に収める。Library ボタンで同じ View 内の一覧へ切り替え、Close で再生画面へ戻る。

機種名や物理ピクセル数は条件に使わない。regular size class と、inactive も含めた division の有無で展開した Duo の機能を判定する。展開した Duo の表示では保存済みの Classic/LED 左配置よりこの配置を優先するが、保存値は上書きしない。iPad と macOS の設定選択は維持する。

Closed で Now Playing を開いたまま展開する経路も調べ、全画面デッキの Display ページに同じ縦・横の構成を適用した。ページ・編集シート・全画面表示の所有者を維持し、ミュート/ミュート解除と出力先(AirPlay)選択も残した。レイアウト変更で PlayerViewModel を再生成したり再読み込みしたりしない。

最終の手動確認で、Portrait 起動→Landscape の実回転後に、再生は続いているのに一覧が空になるケースを再現した。縦横それぞれの枝に付けた LibraryListUpdates が同じ listProjection を所有し、古い枝の onDisappear が新しい枝の更新を cancel し得る構成だった。cancel は入力・スナップショット・表示項目を消すため、次の入力変更まで空の一覧が残る。更新 modifier を縦横の分岐より上の共通親に 1 つだけ置き、同じ原因が各枝で起きる配置を除いた。キャンセルと再計算の所有者は View の切り替えより長く保つ必要がある。

今回の確認結果

Xcode 27.1 Beta(27A9269)、Duo の iOS 27.1 Simulator を使用した。UI テストは Device Hub で実際の向き・姿勢を選んでから実行し、XCTest の向きの通知では代用していない。以下の Duo 3 実行は skip ではなく成功である。

確認 結果
Open・Landscape testLandscapeLEDConsumesRightHalf 成功。root の content 幅 867 pt に対し LED と操作部は各 433.5 pt。右配置、隣接、高さの一致、音量幅、44 pt の再生・調整・音量・ミュートボタンを確認
Open・Portrait testPortraitVisualsStayAboveControlsAndLibraryReturns 成功。content は 669 × 835 pt。上半分の LED とメーターを各 208.75 pt、下半分を操作部に割り当てることを確認
Book・Portrait 同じ Portrait ケースが成功。division は y 373.5 pt・高さ 40 pt、上下 margins 各 20 pt。表示部は y 353.5 pt まで、操作部は y 433.5 pt から配置。LED とメーターは表示部を等分
一覧・設定と再生 Open/Book の両方で Library→Close、Settings→Close を UI 操作。同じ項目 ID・queue・generation・volume・pitch・rate、playing true、時刻の進行と正の左右 RMS を確認
実回転後の一覧 testLibrarySurvivesRealRotationFromPortrait 成功。Portrait→Landscape→Portrait を Device Hub で操作し、横向きの一覧と縦向きから開いた Library の両方に同じ fixture の曲があること、同じ項目・queue・generation と再生状態を確認
共通デッキの展開経路 Closed の Now Playing→Open を手動操作し、全画面デッキの横向き表示を確認。そのまま Portrait/Book へ変えた画面も確認。一覧更新の修正後は Closed の Now Playing→Open Portrait→Library→Close も操作し、3 曲の一覧と再生画面への復帰を確認
関連単体テスト AdaptiveBottomPanel、DuoPortraitPlayerLayout、LibraryBrowsingState、LibraryLayoutPolicy の 33 test definitions・4 suites が成功。幅の境界、追加の出力操作列、ヒンジとカメラの矩形、既存の状態対応を確認
既存プラットフォーム macOS ビルド・上記単体テストが成功。iPhone 17e の iOS 27.0 で testEnglishDarkDeck、iPad Pro 11-inch の iPadOS 27.0 で testMainScreenAndSettingsKeepTabletLayout が成功。一覧更新の所有者を移した後も macOS ビルド・33 単体テストと iPad の UI を再実行し成功
lint・差分 scripts/lint.sh --quiet、git diff --check 成功。既存 warning 42 件、新しい違反や baseline 追加なし

Portrait の Close は、Button の外側だけに高さを付ける実装ではアクセシビリティの操作領域が約 20.3 pt だった。テストの失敗を確認後、ラベルの内側で 44 pt の高さと contentShape を確保し、実際の操作とフレームの再検証が成功した。

パネルの配分は、割り当てた frame に付けた GeometryReader のグローバル座標を使って検証する。SwiftUI のアクセシビリティグループの矩形は、clipped な子要素の範囲まで含むことがあり、領域の幅を測る根拠にはできなかった。DEBUG かつ --ui-testing-duo-layout の起動に限って、<regionID>Metrics に x/y/width/height を返す。通常表示と Release には診断要素を出さない。ボタンのタッチ領域は個々のアクセシビリティフレームで確認する。

最終結果は /private/tmp/DuoRedesignLandscapeMeasuredUITests.xcresult、DuoRedesignPortraitMeasuredOpenUITests.xcresult、DuoRedesignPortraitMeasuredBookUITests.xcresult、DuoRedesignLibraryRotationUIComplete.xcresult、DuoRedesignRotationFixUnitTests.xcresult、DuoRedesignIPhoneRegression.xcresult、DuoRedesignIPadRotationFixRegression.xcresult に保存した。画面画像とテスト添付資料は .build/DuoDesignValidation/ に保存する。展開画面を simctl で撮影するときは io <device> enumerate で表示先を確認する。この環境では screenshot --display=LCD-1 を指定する。既定の LCD は外側画面であり、展開中は黒い画像になる。再現方法は日英の docs/iphone-duo を参照する。

実回転の最初の試行では、Landscape の一覧保持まで成功したが、Device Hub の一時的な noWindowsAvailable と再取得の遅延により Portrait に戻す操作が 45 秒の待機期限を超えた。往復ケースだけ操作者の待機を 120 秒にして再実行し、両方向と一覧操作の最後まで成功した。ほかの回転ケースの待機時間は変更していない。

今回の集中 UI テストは、root の安全領域と画面座標が一致する Open/Book を対象にした。領域内に active な occlusion がある場合、複数または横方向以外の active division がある場合は別ケースが必要であり、skip する。これらの矩形処理は単体テストの確認範囲にとどめる。実機での音切れ、実際の AirPlay 接続、VoiceOver と最大文字での全操作は今回の確認には含めない。毎秒の再生診断と正の RMS だけで実機の可聴品質を確認済みとはしない。

音量行と出力ボタンの外観修正(2026 年 10 月 4 日)

この節は 9a92622(デザインアップデート)を基準にした追加変更の記録である。前節の幅 188 pt 以下という音量行の制約を撤去し、展開した Duo の縦横で音量バーが残りの幅を使うようにした。ミュートと出力先のボタンは 44 × 44 pt を維持し、音量行の右端に固定する。ボタン群の幅を固定し、音量側に .frame(maxWidth: .infinity) を付けることで、左右に余った空間をバーへ配分する。ボタン同士の間隔とパネルの内側余白は残す。

展開した操作部は通常 iPhone の丸いミュートボタンを共用せず、音量の増減と同じ四角い部品を使う。背景は既存の normalButtonFill、角丸 3 pt、輪郭線 1 pt、影は半径 2 pt・下方向 1 pt とし、出力先にも同じ装飾を付けた。音量増減の長押し反復は維持し、ミュートには反復を付けない。既存の Mute/Unmute と音量値のアクセシビリティ文言を使い、日英の Duo ドキュメントも更新した。

出力先は既存の IPhoneRoutePicker/AVRoutePickerView を使い続ける。Apple の仕様では、このビュー自体がボタンであり、タップでシステムの受信機選択を提示する。そのため新たな Button や独自ジェスチャーで囲まず、SwiftUI の背景・輪郭・影だけを足す。輪郭 overlay は .allowsHitTesting(false)、アクセシビリティは .contain とし、内部の標準操作を残した。AVRoutePickerView の仕様

適用先は AdaptiveBottomControlsView を使う root の展開画面と全画面デッキである。通常 iPhone/Closed の円形コントロールと macOS の操作部は変更していない。iPad の .ledHalf 表示も同じ部品を使うため、音量幅と音量増減の影が反映される。

修正後の確認

Xcode 27.1 Beta(27A9269)を使用した。Duo の UI テストは Device Hub で実際の向き・姿勢を選んでから実行した。

確認 結果と範囲
Duo Open・Landscape testLandscapeLEDConsumesRightHalf 成功(24.434 秒)。LED の右半分配置、音量行の左右端、音量→ミュート→出力先の間隔、44 pt のボタンとスライダーを確認
Duo Book・Portrait testPortraitVisualsStayAboveControlsAndLibraryReturns 成功(31.212 秒)。上半分の LED/メーターと下半分の操作部、音量行の左右端と間隔、44 pt の操作領域、Library/Settings からの復帰と再生状態を確認
関連単体テスト AdaptiveBottomPanelTests の 13 tests・1 suite が成功。幅の境界と操作行の配分を確認
macOS ビルド成功
lint・差分 scripts/lint.sh --quiet と git diff --check 成功。既存 warning 42 件。baseline 変更なし

Book の小数座標では 44 pt のフレームが 43.99999999999999 と返ることがあった。UI テストの最小寸法判定に 0.001 pt の浮動小数点誤差許容を加えた。実装のフレームと 44 pt の目標寸法は変更していない。音量行の幅は従来の上限検証から、パネルの内側左端・右端と各操作の間隔を検証する方式へ変えた。

手動操作では Book・Portrait のミュート/解除と、伸ばした音量バーの左端 0・右端 1 を再生状態ログで確認した。Open・Landscape/Portrait の画面も確認し、終了時には確認用再生を一時停止した。標準の AirPlay ボタンがアクセシビリティ階層内に残ることも確認した。ただしシミュレーターでボタンを押しても受信機選択画面は表示されず、装飾を変更していない Closed の同じ標準ボタンでも同様だった。この比較だけでは原因は確定できず、受信機選択画面の提示と実機の AirPlay 接続は未確認である。

結果は /private/tmp/DuoControlsPolishLandscapeUITests.xcresult、DuoControlsPolishPortraitUIComplete.xcresult、DuoControlsPolishUnitTestsComplete.xcresult に保存した。出力先の 44 pt フレームやアクセシビリティ階層の検証は、受信機選択画面の提示や実際の AirPlay 接続の確認とは区別する。

PR #21 の検索・閲覧状態・一括編集のレビュー対応(2026-10-04)

比較元は PR #21 の 8e693a537e888d1c80bd283313f9caab469f92df、修正は 8803ace。追加された3件の指摘はいずれもコードと再現条件に合致していた。

Search タブの条件の適用範囲

検索条件の指摘では、Search に入力した検索語や詳細フィルタが、Library・分類・Videos・Playlists の一覧にも適用されていた。これらのタブには検索条件の入力欄や有効な条件の表示がないため、タブを切り替えると曲が理由なく消えたように見える。

共通の一覧生成処理に isSearch を戻し、Search の呼び出しだけで保存した検索語とフィルタを渡す。その他の一覧には空条件を渡す。検索条件自体は閲覧状態に保持するため、Search に戻ると以前の検索結果を再表示できる。iPad・macOS の検索欄を持つ詳細一覧の処理は変更していない。

プレイリストのルートと共有選択

選択状態の指摘では、プレイリストから戻ってナビゲーション経路が空になっても、共有選択が前のプレイリストを指したままだった。コンパクト表示ではルート一覧が見える一方、展開表示では共有選択から前のプレイリストを再表示する。

プレイリスト経路から選択を設定する処理を共通化し、空経路の場合は既存の削除時と同じ .library(.allSongs) に戻す。この処理を「戻る」操作と保存経路のない Playlists タブの選択に適用する。Library 側の保存経路は維持し、そのタブに戻った際に以前の分類とグループを復元する。

一部削除された一括編集の対象

一括編集の指摘では、対象が全件削除された場合にだけ編集セッションを閉じていた。別の scene から一部を削除した場合、セッションに削除済みモデルが残り、選択件数と保存対象も古いままになる。

対象が1件でもライブラリから消えたら編集セッションを閉じる。macOS と iOS の編集シートは対象配列を値として受け取るため、セッションを終了することで画面内に保持した対象も破棄する。削除の判断には全ライブラリの Query を使い、検索やレイアウト変更によって一覧の射影が一時的に空になっても編集セッションを終了しない。シート終了時には既存の処理で複数選択をリセットする。

回帰確認と確認範囲

Xcode 27.1 Beta(27A9269)を使用した。macOS の関連単体テスト33件が成功し、選択状態・検索とソート・一覧の非同期更新を確認した。追加した3件の状態テストは、比較元の実装に戻すと共有選択の残留2件と一括編集セッションの残留1件で失敗し、修正後は成功した。

検索語の UI 回帰テストも比較元で実行した。Search で1曲に絞ってから All Songs に切り替えると Track 1 が存在せず失敗し、修正後は全3曲を確認できた。詳細フィルタでも、All Songs・Albums のグループ・Playlist で全3曲を表示し、Search に戻った際の条件と結果の保持を確認した。

共通状態の単体テスト15件は、iPhone Duo(iOS 27.1)と iPad Pro 11-inch(M5、iOS 27.0)の双方で成功した。Duo の対象 UI テスト4件もすべて成功し、検索語・詳細フィルタの適用範囲と条件保持、検索後のプレイリスト並べ替え、プレイリストから実際に戻った後の共有選択・空経路・タブの維持を確認した。macOS・iOS のアプリ本体とテストターゲットのビルドも成功した。

再確認では、macOS に LibraryBrowsingStateTests・LibrarySearchAndSortTests・LibraryListProjectionTests、iPhone/iPad に LibraryBrowsingStateTests を指定する。Duo の UI は IPhoneSearchScopeUITests と DuoBrowsingUITests/testCompactPlaylistMovesTheDisplayedTrackAfterSearching・testCompactPlaylistBackClearsTheExpandedDestination を指定する。テストを含む修正コミットから xcodebuild test を実行し、-parallel-testing-enabled NO -collect-test-diagnostics never を使用した。

SwiftLint 0.65.1 は変更した5ファイルで指摘0件。全体ではエラー0件・変更していないファイルの既存警告42件で、baseline は変更していない。差分と関連呼び出し元の独立レビュー、および git diff --check も行った。

初回の既存プレイリスト並べ替えテストは、実際の一覧操作に入る前のシミュレータ回転確認で失敗した。コンパクト表示を強制するケースでは回転が検証条件に不要なため、このケースだけ回転の前提を外した。展開表示の回転検証は維持している。初回のテスト終了後に Xcode が診断収集で待機したため、その実行を中断し、以後は -collect-test-diagnostics never で実行した。

動画を含む検索スコープの UI 試験、別 scene での実削除によるシート終了、実機での開閉は未検証。Videos は空条件を渡す共通処理の呼び出し元として確認した。一括編集の Apply 開始後に削除が競合する場合は今回の修正範囲に含めていない。保存 Task はシートを閉じた後も継続する既存の処理であり、今回のセッション終了を保存処理のキャンセル確認として扱わない。

PR #21 の追加点検と回帰修正(2026-10-04)

レビュー対応後の 8803ace と PR の比較元 main を使い、共有閲覧状態、レイアウトの切り替え、再生・シーク、編集・インポート・書き出しの呼び出し元を追加点検した。iOS 27.1 の新 API は採用 SDK の Swift interface と可用性ガードを照合した。追加で確認した4件の原因と修正を記録する。修正は 94e3f44。

タブが変わる際の一覧操作状態

Search と空の Playlists ルートは、共有選択として .library(.allSongs) を使う。そこから展開表示のサイドバーで All Songs を選ぶと、選択値が同じという理由で操作状態をリセットせず、前の画面の選択・スクロール位置・複数編集モードが残っていた。select は選択値とグループに加え、変更前後の phoneTab も比較する。Search と Playlists の2ケースで、選択値を維持したまま表示先が変わる条件をテストした。

プレイリスト削除と独立した Library 経路

Library に Artist のグループを保存してから Playlists に移り、現在のプレイリストを削除すると、削除処理の select(.library(.allSongs)) が無関係な libraryPath まで上書きしていた。削除対象の選択・プレイリスト経路・操作状態だけを更新し、Library の保存経路を維持する。直接削除と全 Query の ID から検出する削除の両方で、Library に戻った際に以前のグループを復元できることを確認した。

全項目が消えた分類グループ

保存したアルバム・アーティスト・ジャンルの全曲を削除した場合、コンパクト表示のナビゲーション経路と共有グループが残っていた。子画面が消えてから展開表示の AlbumBrowser が初めて表示される経路では、子の非初期 onChange では失効処理に到達できず、保存したタイトル・再生キューと表示内容が食い違う。

安定した親の MainView で、保存グループが全ライブラリに存在するかを初期表示から監視する。分類一覧は音声だけを含むため、同名の動画が残ってもグループの生存と見なさない。現在のグループが消えたら分類へ戻して操作状態をリセットし、別タブを表示中なら Library の保存経路だけを修正する。除去を適用する時点でも同じグループが経路に存在するかを確認し、新しく開いたグループを古い判定で閉じない。検索の一時的な射影を全削除の根拠に使わない。AlbumBrowser の既存の検索結果変更による選択解除とは別の検証である。

専用テストは3分類を対象とし、全ライブラリが空・別グループが残存・同名の動画だけが残存する削除、Playlist/Search 表示中の削除、検索条件下での生存、最後の項目の分類名変更、古い除去判定の適用を確認する。ID が変わらない名称変更でも消失を検出する。

遅れた音声エラーと現在の動画読み込み

音声から動画に切り替えた後、以前の音声エンジンのエラーが MainActor へ届くことがある。今回の PR が加えた playbackGeneration をそのエラーが進めると、現在の動画読み込みの完了が世代不一致で破棄され、再生を開始できない。音声エラーの適用は音声モードに限定し、現在の動画の世代と再生状態を変更しない。現在の音声のエラー処理は維持する。

テストでは動画の読み込みを continuation で保留し、読み込み開始後に古い音声エラーを渡してから完了させる。完了前後の世代、対象 ID、エラー表示、再生開始回数を確認し、動画再生開始後の音声エラーも無視することを確認した。実際の音声エラー発生と通知の遅延をシミュレータで偶然起こす試験ではない。

検証結果と再確認方法

Xcode 27.1 Beta を使用した。タブ変更・プレイリスト削除・遅れた音声エラーの追加テストは、該当箇所だけを修正前に戻すとそれぞれ失敗し、修正後は成功した。タブ変更は2例で8個、プレイリスト削除は2例で6個、動画読み込みは8個の期待値が修正前に失敗した。

グループ削除は UI でも修正前の失敗を確認した。MainView の新しい監視処理だけを外し、コンパクト表示で Albums→Layout Album と開いて、確認ダイアログを通して全3曲を削除した。全行が消えた後も共有 groupName が残り、分類への復帰を待つテストが失敗した。このケースは同じ scene の Query 更新を使うため、別 scene からの実削除・レイアウト変更との競合の実行確認とは区別する。

環境 確認結果
macOS 関連112 tests・15 suites 成功。閲覧状態、グループ失効、再生状態と世代、一覧の射影・検索・ソート・削除、レイアウト判定、アルバム・プレイリスト、共有書き出し、歌詞と編集キャンセルを対象
iPhone Duo/iOS 27.1 関連98 tests・13 suites 成功。macOS と共通の閲覧・再生・レイアウト・共有書き出し等を対象
iPad Pro 11-inch(M5)/iOS 27.0 閲覧状態、グループ失効、再生状態と世代の36 tests・4 suites 成功
Duo の対象 UI 4ケース成功。アルバム全曲削除後の Albums 復帰、プレイリストから戻る共有状態、検索語・詳細フィルタのスコープと条件保持を確認
lint・差分 全体42警告・エラー0。追加点検前と警告のファイル・ルールの組は同じで、新しい違反と baseline 追加はない。git diff --check 成功

lint で追加テストの型が長さの上限を超えたため、同じファイルの extension に分けて解消した。整理後も macOS の閲覧状態17 tests と iPad の上記36 tests で、追加のテストが検出・実行されることを確認した。

再確認には LibraryBrowsingStateTests・LibraryBrowsingGroupInvalidationTests・PlayerPlaybackGenerationTests・PlayerPlaybackStateTests を指定する。広い回帰確認では上表の関連 suites を追加し、xcodebuild test -parallel-testing-enabled NO -collect-test-diagnostics never を使う。今回の結果は .build/pr21-extra-mac.xcresult・pr21-extra-duo.xcresult・pr21-extra-ipad.xcresult、修正前の比較は .build/pr21-extra-before-state.xcresult・pr21-extra-before-playlist.xcresult・pr21-extra-before-player.xcresult に保存した。

削除 UI は修正後に成功し、共有グループの解除・保存経路・選択・タブと Albums のナビゲーションタイトルを確認した。対象は DuoBrowsingUITests/testCompactDeletingTheLastAlbumTrackReturnsToAlbums・testCompactPlaylistBackClearsTheExpandedDestination と IPhoneSearchScopeUITests である。結果は .build/pr21-extra-duo-ui.xcresult と pr21-extra-query-ui-final.xcresult、削除の修正前比較は pr21-extra-before-group-ui.xcresult に保存した。

最初の4ケースの実行では3ケースが成功したが、検索語のケースは検索結果の検証に入る前にキーボードの Search ボタンを操作できず失敗した。Simulator がボタンを画面外の座標として返し、アクセシビリティの scroll-to-visible に失敗したためである。検索欄へ Return を入力して同じ検索確定操作を行うようテストの helper を変更し、そのケースを再実行して成功した。アプリの検索処理には追加変更していない。

別 scene での実削除からレイアウトを切り替える操作、実機の開閉、ライブラリが非常に大きい場合の応答性は、この追加点検では実測していない。グループの生存判定は保存経路がある場合のメタデータ比較であり、同期ファイル I/O・メディア解析・明示ロックは呼び出していない。世代テストの成功を実機での音切れや通知遅延の検証として扱わない。

PR #21 の Details ページ選択のレビュー対応(2026-10-04)

比較元は 94e3f44。Details シートの指摘は妥当だった。iOS の inline Details は共有 panelContent を使う一方、IOSLibraryPresentations のシートは LyricsPanelView のローカルな .lyrics を使っていた。そのため inline の Information 選択をシートに引き継がず、シートで選んだページも共有状態へ反映しない。修正は 36b2910。

Details の LyricsPanelView にページ選択の Binding を渡し、Picker と本文の表示条件を同じ Binding から取得する。browsingState 全体は渡さない。全体を渡すと、シート内からの情報・歌詞編集、画像拡大も root の別シートへ要求する経路に変わるためである。Details 自身は安定した親のシートであり、内部の編集シートと popover の提示元を維持したまま、ページ選択だけを共有できる。情報編集から戻った後の再読み込みも既存のローカル onDismiss を使う。

全3箇所の LyricsPanelView 呼び出し元を点検した。iOS の inline は従来どおり共有閲覧状態を使い、macOS はローカル選択を維持する。全画面デッキは親が .lyrics/.information を固定して渡し、独立したページ選択を所有する。同種の未伝達は見つからなかった。シート・全画面の提示元、情報と歌詞の編集、画像プレビュー、読み込みの項目・世代検証も独立して点検し、追加の確定した新規不具合は見つからなかった。

回帰確認

DetailsSheetUITests を追加し、アクセシビリティ文字サイズで inline Details を使わずシート表示にする。自動再生する長いローカル音声 fixture を使い、再生を一時停止して対象を固定する。Info を選択し、情報読み込みが完了した後にシートを閉じ、共有状態の panelContent が Information に更新されたことと、再表示時の Picker と本文が Info のままであることを確認する。別ケースでは情報編集を開いて Close、歌詞編集を開いて Cancel を操作し、いずれも Details に戻ることを確認する。

修正前の比較では contentSelection の受け渡しだけを外した。Info 選択・情報表示・シート終了までは成功したが、root の panelContent が Lyrics のままで、Information を期待する UI テストが失敗した。比較結果は .build/pr21-details-before-selection-ui-5.xcresult に保存した。macOS は閲覧状態、レイアウト判定、歌詞の読み込み、メタデータ下書きのキャンセルに関する34 tests・4 suites が成功。iPad Pro 11-inch(M5、iOS 27.0)は関連20 tests・2 suites と上記 UI 2ケースが成功し、結果は .build/pr21-details-ipad-final.xcresult に保存した。

Details のアクセシビリティ識別子は children: .contain を指定したグループに付け、子の Picker の識別子を上書きしないようにする。iPad のシートは中央のポップアップとして表示され、Picker 上のドラッグでは閉じなかった。PopoverDismissRegion を確認して、十分な余白がある場合はシート外側をタップする。Duo ではほぼ画面全幅となり、手動座標による背景タップで閉じる操作を再現できなかった。終了要素の isHittable を確認し、PopoverDismissRegion.tap() でシステムの hit point を使うと、終了・共有状態確認・再表示が成功した。元のドラッグは Picker 上から始まっており、シート自体が操作で閉じられないことの根拠にはしない。

Duo の Simulator は XCTest の landscape 要求後も実表示が portrait のままになることがある。このテストは回転を検証しないため、診断値で実際の portrait 表示を確認した場合は XCTest の向きを portrait に合わせる。実機の開閉や回転の成功とは区別する。

Duo/iOS 27.1 でも閲覧状態とレイアウト判定の20 tests・2 suites、および最終の UI 2ケースが成功した。単体テストの結果は .build/pr21-details-duo-final.xcresult、終了操作の修正後に UI 2ケースをまとめて再実行した結果は .build/pr21-details-duo-ui-verified.xcresult に保存した。最初の UI 実行は編集の往復が成功し、選択保持のケースはシート終了操作で失敗していた。成功した最終実行と区別する。macOS の結果は .build/pr21-details-mac.xcresult。

最終の全体 lint は226 files で42警告・エラー0。変更前と警告のファイル・ルールと件数を照合し、新しい違反はなく、変更した4ファイルへの strict lint も0 violations。baseline は変更していない。git diff --check も成功した。

今回の UI 試験はアクセシビリティ文字サイズでのシート再表示と入れ子の編集画面の往復である。実機の開閉、inline からシートへ切り替わる実際のリサイズ操作、画像拡大の実操作は含めない。inline が同じ panelContent を使うこと、Details の画像プレビューが既存のローカル popover 経路を維持することは呼び出し元とコードで確認した。

PR #21 の Commands と表示切替のレビュー対応(2026-10-04)

比較元は 36b2910。メニューの表示切替の指摘は妥当だった。AppMenuCommands は macOS に限定されず、iOS の展開表示でも DetailAreaView が提供する AppMenuActions を使う。Details/Equalizer のコマンドだけが inline 用の設定値を直接反転していたため、幅・高さ・アクセシビリティ文字サイズによって inline 表示が許可されない場合、ショートカットを押しても何も表示しない。修正は 6a48030。

Commands をツールバーと同じ toggleDetails()/toggleEqualizer() に接続する。Commands の Show/Hide 判定にはシートの提示状態も含め、切替関数は提示中のシートを先に閉じる。シートの提示中に inline の可否が変わっても、Hide が背後の設定変更にならないようにする。macOS は従来どおり inline の設定値を切り替える。

コンパクト表示の iPad のメニュー提供元

追加点検で、コンパクトになった iPad に AppMenuActions・情報表示・AAC 変換の提供元がないことを確認した。PR の MainView は端末種別ではなく size class で IPhoneLibraryView を使う一方、再生 Commands の代替処理は phone に限定されていた。現在曲があっても iPad の Space/Command+左右キーが無効になり、選択項目の情報表示やライブラリ操作も無効になる。

コンパクトの根と表示中の一覧に、既存の操作先を使う Commands の提供元を置く。一覧では既存の非同期 projection とグループの再生キューを再利用し、新しい同期ソートや I/O を加えない。情報表示・AAC 変換は表示中の projection に選択 ID が存在するか、動画・編集・提示状態の条件を確認し、操作を適用する時点でも確認する。

TabView が保持する別タブ、NavigationStack が保持する親カテゴリは提供元にしない。現在のタブと経路の末尾から操作対象の画面を判定し、その画面だけが scene value を提供する。操作の提供元は別のゼロサイズの view に置き、一覧・ナビゲーション・projection 自身の identity を変更しない。取得済みの callback にも現在の経路を確認する guard を置き、切替後の画面を古い操作で変更しない。

全画面動画から一覧へ戻る操作

全画面動画の提示元を安定した親に移したため、player.showVideoArea だけを false にしても browsingState.showsVideoFullScreen が残る。Commands の Back to List は inline・全画面のいずれも動画表示として判定し、両表示フラグを閉じる。項目・キュー・再生状態を変更する停止操作にはしない。

コンパクトへ移行した後も親の全画面フラグが残る場合は、Compact の Commands から同じ親の表示を閉じる。Deck の提示状態だけで判定しない。Deck/親の全画面表示中は、背後のライブラリから別のシートを提示する操作を無効にする。再生・曲送り・Back to List は有効に保つ。提示前に取得した callback についても、適用時の表示状態を確認する。

検索キーボードで Duo の Library が閉じる問題

Duo の縦向き Library で検索欄を選ぶと、キーボードを避ける root の GeometryReader が高さ558・幅669の横長となる。DuoPortraitPlayerLayout.isPreferred が false に変わり、別の desktop 分岐へ切り替わる。onChange(of: portraitPlayer) は showsPortraitLibrary を false にするため、キーボードが閉じて縦向き分岐に戻っても再生画面を表示し、開いていた Library が消える。最初のショートカット試験で Search をフォーカスした際に見つかったが、通常の検索操作でも再現する不具合である。

06f95c2 は、背景の測定用 GeometryReader だけに ignoresSafeArea(.keyboard) を指定する。向きの判定にはその寸法を使い、本文は元の GeometryReader の提案とキーボード回避を維持する。測定は継続するため、回転や表示領域の変更を固定寸法で判定しない。背景と本文の扱いは Apple の Adding a background to your view と ignoresSafeArea の契約に従う。

修正前の向き判定に戻した UI 比較では、Search を選んだ後にキーボードの存在確認が失敗した。終了時の AX 記録で duoCloseLibrary がなく duoShowLibrary が現れ、Library が再生画面へ戻っていることを確認した。結果は .build/pr21-command-duo-keyboard-before.xcresult。修正後はキーボードの存在、本文の高さが幅より小さいこと、検索欄の hit target、検索の絞り込みを確認し、Library を表示したまま入力できた。iOS 27.1 の Search 終了ボタンは AX のラベルが小文字の close であり、これを押してキーボードを終了する。終了後も Library と再生中項目・キュー・読み込み世代・一時停止状態を保持した。

修正前との比較

アクセシビリティ文字サイズの展開 iPad で Command+Option+L/E を入力し、Details/Equalizer のシートが現れないことを確認した。比較結果は .build/pr21-command-before-ipad-ui.xcresult。コンパクト用の scene value の提供元だけを外した比較では、選択項目の Command+I と、現在曲に対する Space による一時停止が失敗した。比較結果は .build/pr21-command-before-compact-ui.xcresult。

全画面動画の比較は UI のキー入力ではなく、実際の DetailAreaView.appMenuActions を mock の再生サービスで呼ぶ単体テストである。動画の操作関数と visible 判定だけを修正前へ戻すと、inline 動画の有無と再生/一時停止の4条件で全画面フラグの残存等の10期待値が失敗した。比較結果は .build/pr21-command-before-video.xcresult。このテストの成功を、実際の全画面 view へのキー配送や実機の開閉確認として扱わない。

最終確認

Xcode 27.1 beta を使用した。macOS は閲覧状態17・レイアウト3・Duo の純粋なレイアウト判定5・再生状態7・再生世代6の計38 tests・5 suites が成功し、結果は .build/pr21-command-mac-final.xcresult に保存した。iPad Pro 11-inch(M5、iOS 27.0)と iPhone Duo(iOS 27.1)は、同じ38 tests と表示切替7 tests の計45 tests・7 suites がそれぞれ成功した。表示切替の7 tests は DetailArea の全画面から戻る操作1 test(4条件)と、Compact の画面判定・全画面復帰・提示抑止・古い経路の操作拒否6 tests である。最終の結果は iPad が .build/pr21-command-ipad-complete.xcresult、Duo が .build/pr21-command-duo-complete.xcresult。

iPad の UI は、アクセシビリティ文字サイズで Command+Option+L/E により Details と Equalizer を各2回開閉する2ケース、コンパクト表示で Space と Command+左右キーを操作する1ケース、Command+I で選択した曲の情報を開き、検索で非表示になった選択曲を開かない1ケース、プレイリストの根とアルバムカテゴリで Command+I を無効にする1ケースの計5ケースが成功した。開閉2ケースの最終結果は .build/pr21-command-ipad-complete.xcresult、コンパクトの再生と検索の2ケースは .build/pr21-command-ipad-compact-final.xcresult、根とカテゴリは .build/pr21-command-ipad-roots-final.xcresult に保存した。

Duo の UI は Open の縦向きで、アクセシビリティ文字サイズのツールバーから Details と Equalizer を各2回開閉する2ケースと、検索キーボードの提示・終了に伴う Library/再生状態の保持1ケースの計3ケースが成功した。3ケースをまとめた最終結果は .build/pr21-command-duo-ui-complete.xcresult。最初の EQ 終了操作は Library の同名 Close を選んでいたため、EQ の navigation bar 内に限定した。検索終了操作も、存在しない Cancel から記録済みの close に合わせた。いずれも実装側の未修正不具合と扱わず、最終結果と途中の UI ハーネスの失敗を区別する。

Duo のキー入力については、L/E に加えて、FocusedValue に依存しない既存の Command+M、別の CommandGroup に属する Command+O も作用しなかった。診断結果は .build/pr21-command-duo-dispatch-probe.xcresult と .build/pr21-command-duo-group-dispatch-probe.xcresult。キー送信が成功したという XCTest のログだけでは Commands の実行を確認できない。Apple の CommandGroup は iOS でもショートカットを提供するため、iPhone 自体が未対応とは判断しない。この Simulator 環境でのキー配送は未検証として記録し、ショートカットの UI ケースは iPad に限定する。Duo では同じ表示切替 helper を呼ぶツールバーを実操作で検証した。

過去の xcresult には成功したケースと途中の UI ハーネスの失敗が混在する。最初のコンパクト情報表示ケースは Search タブの検索欄が折りたたまれていたため入力できず、検索を展開する操作を追加した。検索入力中の iPad はタブを隠すため、検索のケースを根・カテゴリのケースと分けた。根のケースが最初に失敗したのは、タブ切替でも項目選択が残ると誤って期待していたためである。既存の resetListInteraction() は選択を解除する。最終ケースは解除を確認してから情報表示の無効化を検証する。検索条件の変更では選択 ID が残るため、非表示となった項目に操作しないことを独立して検証した。

XCTest は一部のキー入力・検索・キーボード終了操作でアニメーション完了通知を60秒待った後に続行した。最終の期待値は成功しているが、これらの成功を応答時間の測定結果にはしない。実機の開閉、ウインドウのリサイズ、全画面動画へのキー配送、AAC 変換・書き出しのメニューからの実行は今回の UI 試験に含めない。全画面動画の再生保持と取得済み callback の拒否は mock の単体テスト、AAC・書き出し等の呼び出し先は既存経路とのコード比較で確認した。

最終の全体 lint は232 files で42警告・エラー0。変更前と警告のファイル・ルール・件数を照合し、増減はなかった。変更した11ファイルへの strict lint も0 violations。baseline は変更していない。git diff --check とステージ済み差分の検査も成功した。関連する呼び出し元、状態遷移、項目の削除と非同期結果の適用条件を独立して再点検し、今回修正した箇所以外の確実な新規不具合は見つからなかった。

2026-10-04: 検索でアルバムが非表示になった場合の共有経路

追加レビュー 4176380225 は妥当と判断した。AlbumBrowserView の items は検索・詳細フィルター適用後の一覧である。アルバム A を閲覧中に B の曲だけが残る検索をすると、albums.map(\.id) の監視が A の選択を解除する。PR でローカル State を共有経路に変更したため、この解除は closeGroup() を通じて保存経路、複数選択、編集モード、一覧位置までリセットする。検索の解除後も A に戻れなくなる。

前回のセルフレビューで見落とした理由

以前の「全項目が消えた分類グループ」の調査では、未フィルターの Query による削除判定を追加し、既存の AlbumBrowser の検索結果変更による選択解除を別の検証として扱った。その際、ローカルの選択解除から共有状態の破棄へ意味が変わった setter の呼び出し元を最後まで追わなかった。この区別は不適切だった。

既存の削除判定の単体テストは、全ライブラリに残る項目を missingLibraryGroup(in:) に渡していた。共有状態の判定自体は正しくても、実画面の onChange から別の解除が起きることは検証できない。また、UI fixture の3曲は同じアルバムに属していた。全曲不一致の検索では親の items.isEmpty 分岐が AlbumBrowser を取り除くため、「A だけを隠して B を残す」という監視処理の発生条件を試せていなかった。

修正と同じ原因の点検

検索結果のアルバム ID を根拠とする解除と、選択 ID の暗黙の setter を除去した。閲覧先がある限りアルバム詳細を表示し、対応する検索結果がない間はアルバム名・戻る操作・既存の「No Media」文言を表示する。検索結果が空の間は再生操作を表示しない。明示的な戻る操作と、安定した親の全ライブラリ Query による実削除・名称変更だけが経路を解除する。

検索条件を保持したまま展開画面を初めて表示する経路も確認した。旧監視は initial を指定しないため、その時点では共有経路 A が残っても selectedAlbum == nil を理由に B のグリッドを表示していた。今回の閲覧先 ID を優先する分岐は、この初期表示の不整合も防ぐ。

親の MediaListView も選択グループを空一覧判定より先に扱う。これによりアルバム・アーティスト・ジャンルの検索結果が全体として空の場合と、別グループの結果だけが残る場合の両方で戻る操作を維持する。

コミット前に、検索結果の更新・投影の cancel・画面の onDisappear・シートの setter・項目とプレイリストの retain から共有状態を書き換える経路を点検した。MainView の項目・プレイリスト・グループ失効判定は全 Query、compact の複数選択 retain も親の全項目を入力としており、検索結果を削除と誤認する追加経路は見つからなかった。MediaListView の選択項目の同期は既存の操作であり、経路や編集セッションを解除しない。空の詳細画面への変更は既存 API と翻訳キーだけを使い、新たな同期 I/O やメディア解析を追加しない。

共有状態へ移行するセルフレビューでは、移したプロパティの宣言だけでなく、旧ローカル状態の全書き込み元とその入力が原本・全 Query・検索結果のどれかを確認する。回帰テストの fixture には「選択先が消えるが他の表示結果は残る」条件を用意し、状態モデルの判定と実画面の observer を区別して検証する。

発生条件に対応する回帰テスト

通常の fixture は維持し、--ui-testing-multiple-albums 指定時だけ3曲目を別のアルバム・アーティスト・ジャンルにした。ExpandedGroupFilteringUITests は iPad の展開画面で2曲のグループを開く。通常検索では3曲目だけが合致する条件と、全曲不一致の条件を順に適用する。検索解除後の2曲の復帰、保存経路・分類・タブ、空結果中の戻る操作、別グループの表示へ切り替わらないことを確認する。アルバムの通常検索では、先に複数編集モードで1曲を選び、検索と解除を通じた複数選択・編集モードの保持も検査する。アーティスト・ジャンルの通常検索も別のケースにした。詳細タイトルフィルターは、指摘の直接の発生条件である3曲目だけが残る絞り込みと解除を確認する。

production の2ファイルだけを HEAD に戻した修正前比較では、アルバムの通常検索を実行し、選択先の曲が消えた後にグループ内の空表示が維持されず、No Media の存在確認が失敗した。結果は .build/pr21-album-filter-before-ipad.xcresult。共有経路と複数選択を解除する原因は、旧監視から closeGroup()・resetListInteraction() までのコードで確認した。修正前のテストは空表示の最初の失敗で停止するため、後続の経路・複数選択 assertion を実行した結果とは区別する。

途中の UI 試行では、空の入力欄への Command+A の後にアニメーション完了通知の60秒待ちが繰り返された。また、検索欄の Return は検索 UI やフォーカスの解除を保証せず、ソフトウエアキーボードで本文が縮むと、アルバム詳細の LazyVStack にある行や空表示がアクセシビリティに現れなかった。検索の解除とフォーカスの解除は Apple の Managing search interface activation でも区別されている。途中の UI 操作の失敗は、共有経路が失われる不具合の検証結果と区別する。

通常検索は検索欄の Clear を操作し、キーボード終了後に本文の高さが回復したことを確認する。シミュレーターでは Keyboard 要素が取得できなくても、Hide keyboard ボタンがアプリ全体のボタン一覧に存在した。最終ハーネスはこのボタンを直接取得し、操作可能状態・終了キーの消失・本文の高さの回復を待つ。座標による終了操作は除いた。

詳細フィルターではキーボード終了後に Done を押す。途中の .build/pr21-album-filter-ui-semantic.xcresult では、画面上でシートが閉じて No Media を表示していても、背後のツールバーの AX 値が Visible のままだった。終了判定は実際の Done の消失・操作不能状態を使い、次のフィルター表示と解除、曲の復帰まで検証する。この AX 値の不一致を、共有経路の喪失や Done が実行されない不具合と同一視しない。コードと SDK の toolbar builder も点検したが、共有状態の参照が Observation から外れる確定原因は見つからず、SwiftUI の AX 更新か XCTest の snapshot cache かは特定していない。

最終確認

Xcode 27.1 beta を使用した。macOS と iPad Pro 11-inch(M5、iOS 27.0)で、閲覧状態・グループ失効・一覧の非同期投影・アルバム集計の40 tests・4 suites がそれぞれ成功した。結果は .build/pr21-album-filter-mac-verified.xcresult と .build/pr21-album-filter-ipad-clean.xcresult。UI ハーネスの途中の失敗を含む後者の bundle 全体が成功したという意味ではなく、対象単体テストの成功である。

通常検索と複数選択の保持、およびアーティスト・ジャンルの検索と解除は .build/pr21-album-filter-ui-semantic.xcresult の各ケースで成功した。これらの2ケースは A を隠して B を残す条件と全件不一致の条件を含む。詳細フィルターは終了判定を修正したハーネスで、A を隠して B を残す条件と解除が成功した。結果は .build/pr21-album-filter-advanced-verified.xcresult。全件不一致の詳細フィルターは、XCTest の60秒の待機が繰り返されたため最終 UI ケースに含めない。途中の .build/pr21-album-filter-advanced-final.xcresult は中断結果であり、完了したテストの成功とは扱わない。実機の開閉・ウインドウのリサイズ・フィルター保持中のレイアウト移行も今回の UI 試験に含めず、経路を入力とする表示分岐と既存の状態テストで確認した。UI の成功を応答時間の測定結果にはしない。

全体 lint は233 files で42警告・エラー0。変更前の警告とファイル・ルール・件数を照合し、増減はなかった。変更4ファイルの strict lint は違反0、baseline の変更はない。

縦バー向けのツールバー整理と idiom 判定(2026年10月14日、#80)

#80 で、Apple の Duo 向け資料(Technology Overviews、prepare ページ、HIG、Tech Talk 111461/111462)と照合して残っていた不足点に対応した。比較元は f600ed4。Xcode 27.1 RC(27A9275)を使用した。

ツールバーの構成

ライブラリのルート、曲一覧、デッキにあった独自の「More」(ellipsis の Menu)を ToolbarOverflowMenu に移した。省略記号のボタンはシステムの overflow ボタンだけになり、項目が収まらない場合に「…」が二重にならない。曲一覧の HStack は Add Tracks、Done、overflow の個別の項目に分けた。デッキの Close と Edit Lyrics…、調整シートの Close から .frame(minWidth: 44, minHeight: 44) を、デッキの More から .labelStyle(.iconOnly) を外し、custom view として扱われないようにした。

visibilityPriority(.high) は Filters、Add Tracks、Done、デッキの Close と Edit Lyrics… に付けた。状態を示す項目と、画面を閉じる・モードを抜ける項目である。Import や Play from Music などは従来どおり overflow に置く。macOS と共有する DetailAreaView+Toolbar.swift は変更していない。

Close だけのシート(設定、再生の調整、イコライザー2か所)は、iOS 27.1 以降で toolbarVerticalBehavior(.disabled) を指定する。修飾子は NavigationStack の内側の内容に付ける。

idiom 判定の置き換え

AppMenuCommands は、AppMenuActions がないとき UIDevice.current.userInterfaceIdiom == .phone の場合だけ Space/Command+左右キーで player を直接操作していた。調べると、compact の画面(iPhone と compact の iPad)はデッキ表示中も含めて CompactLibraryCommandValues が、展開した画面は DetailAreaView が操作を提供している。提供元がないのは展開した Duo の縦向き再生画面(expandedIOSView の portraitPlayer)で、idiom 判定はこの画面の再生操作を補っていた。

新しい FocusedValue allowsDirectPlaybackCommands を iOS の MainView の root から focusedSceneValue で公開し、idiom の代わりに使う。macOS は公開しないため、補助ウインドウでは従来どおり無効になる。compact の iPad でも提供元がない場合は player を操作するようになるが、compact の画面は通常提供元を持つ。

確認結果

確認 結果
ビルド macOS、Duo(iOS 27.1)、iPhone 18 Pro と iPad Pro 13(iOS 27.0)で成功
単体テスト macOS の SimpleMediaPlayerTests 824件成功。iPhone 18 Pro で CompactLibraryCommandDestinationTests、DuoPortraitPlayerLayoutTests、LocalizationTests の20件成功
キーボードコマンド iPad(27.0)の CompactLibraryCommandsUITests/testCompactIPadPlaybackCommandsPauseAndChangeTracks 成功
overflow を使う UI テスト iPhone 18 Pro(27.0)で変更した25件を実行。初回は testExportUsesShareSheet が共有シートの終了前に overflow ボタンを探して失敗し、終了を待つよう直して成功。Duo の Closed では testSettingsAndBulkEditing、testExportUsesShareSheet、Music の入口のケースが成功
lint scripts/lint.sh は変更前と同じ38件。baseline は変更していない

Duo の Closed(外側画面 466 × 678 pt)で、一時的な UI テストから各画面を撮影し、要素の座標を記録した。画像は .build/DuoToolbarScreens/ に保存した。

  • ライブラリのルート、曲一覧、プレイリスト、Search、デッキで、overflow ボタンは右端の縦の列(x ≈ 396〜440)に並ぶ。省略記号は各画面に1つだけである。overflow ボタンの大きさは縦の列で 38 × 38 pt、iOS 27.0 のナビゲーションバーで 36 × 36 pt。システムの寸法であり、44 pt には満たない。
  • プレイリストでは戻る、Add Tracks、overflow の順に縦に並ぶ。Search の Filters も縦の列に入り、キーボード表示中も残る。
  • 文字だけの Done は縦の列ではなく、上端の横のバーに置かれる。#80 の調査で確認した Apple の説明(文字だけの項目は縦に並ばない)どおりの動作であり、このままにした。
  • 設定と再生の調整のシートは、修飾子ありでは横のバー(タイトル中央、Close は右端 x = 369)になる。修飾子を一時的に外して撮影すると、ツールバーの右側に列を確保し、Close が x = 309 に移り、フォームの幅が狭くなった。修飾子が効いていることを外した状態との比較で確認した。

未検証の条件と見つかった既存の問題

  • Device Hub が必要な確認は #81 に切り出した。
  • Closed の横向き:XCUIDevice.orientation を変更しても window が 466 × 678 pt のまま変わらなかった。横向きで overflow に移る項目と、キーボード表示中の overflow は Device Hub での回転が必要である。
  • イコライザーのシート:Closed のデッキからは開けず、Command+Option+E でもライブラリのシートは開かなかった。2か所とも未確認。
  • Duo 縦向き再生画面の Space:DuoPlayerDesignUITests/testPortraitPlayerSpaceTogglesPlayback を追加したが、Device Hub で Open/Book の縦向きにしないと skip する。提供元を外したときに失敗することも未確認。
  • Open での左右の Split View、Book のシートとポップオーバー、内側カメラ使用中の occlusion、Reduce Transparency・VoiceOver・Reduce Motion、実機・AirPlay は未実施。
  • ToolbarOverflowMenu の項目は、iOS 27.0/27.1 とも accessibilityIdentifier を保持しない。UI テストは overflow ボタンの識別子の末尾 OverflowBarButtonItem と項目の表示名で探す(ToolbarOverflowUITestSupport.swift)。
  • 既存の問題(#82):複数選択中の .safeAreaInset(edge: .bottom) の Edit Selected バーは、背景の右側約70 pt が縦のタブバーの下に入る。Closed のデッキの Display ページは縦スクロールの構成で、再生ボタンの列が初期表示では下端で切れる。いずれも今回の差分より前からの動作である。
  • Duo の Closed では、app.navigationBars.buttons で戻るボタンを探す既存の UI テスト(LibraryArtworkUITests の iPhone 経路、testAACResultSurvivesClosingDeck)が失敗する。変更前の HEAD でも同じように失敗した。

展開画面の下端で隠れる検索・フィルター(2026年10月15日)

Duo の Open 横向きで、右側の縦ツールバーの下端がシークバーと再生パネルに覆われた。desktopView の VStack では NavigationSplitView の割り当て高が下部パネルの分だけ短くなる。DetailAreaView に付けた .searchable(placement: .toolbar) の浮動検索ボタンはその下端ではみ出し、重なり順だけを上げても円の枠だけが見えてアイコンが欠けた。

最終的な変更は以下の3点。

  • iOS の libraryNavigation に .zIndex(1) を付け、浮動ツールバーを下部パネルより手前に描く。
  • iOS の Filters に .visibilityPriority(.high) を付け、使用頻度の高い検索条件の入口を overflow より優先する。
  • 展開した iOS の .searchable を DetailAreaView から expandedIOSView の画面全体へ移す。標準の検索欄はサイドバー側に配置され、下端で切れていた右側の検索ボタンを使わずに入力できる。検索文字列は引き続き同じ LibraryBrowsingState.searchText に保存する。compact iPhone の検索と macOS の検索の提示元は従来どおり。

Xcode 27.1 RC / iOS 27.1 の Duo を Device Hub の内側画面で直接操作し、フィルター画面の開閉、検索入力で一覧が空になること、検索をクリアすると元の曲が戻ることを確認した。検索欄の移動は、下部パネルの寸法やプレイヤーの所有を変えずに浮動検索のクリップを避けるための設計判断。

DuoToolbarUITests に検索・フィルター・再生操作の回帰ケースを追加した。この環境では XCTest のスクリーンショットが非表示のディスプレイを取得して黒い縦向き画像になる。内側画面の AX 要素は取得できても isHittable は使用できないため、実際の横向き画面とキャプチャの向きが合わない場合は skip する。UI runner の起動待ちも長くなる場合があるため、この入力テストは TEST_RUNNER_DUO_TOOLBAR_INPUT_TESTS=1 を指定した場合だけ実行する。操作検証は Device Hub での直接操作と区別して記録する。UI runner の失敗をボタンの回帰と判断する前に、対象ディスプレイを確認すること。

Book・Closed への実際の開閉、回転中の検索、ソフトウェアキーボード、最大アクセシビリティ文字サイズは今回の再検証対象に含めていない。

最終コードの検証では、Duo 向け iOS ビルドと macOS Release ビルドが成功。iPad Pro 13(iOS 27.0)単独の test-without-building で LibraryLayoutPolicyTests・AdaptiveBottomPanelTests の16件と IPadLayoutUITests/testMainScreenAndSettingsKeepTabletLayout が成功した。lint は既存の38警告、エラー0で変更ファイルの新規違反はない。Duo の入力 UI テストはキャプチャ/ターゲットの制約により直接操作で代替し、複数 destination のテスト実行は中断後に iPad 単独で再確認した。クリーンビルド前の途中生成物では起動時にクラッシュしたことがあったが、クリーンビルド後の実画面の起動と操作は成功している。ログはアプリの .build-logs/toolbar-final-ios-*、toolbar-final-macos-*、toolbar-final-ipad-*、toolbar-final-lint.log に保存した。

最終コードで DuoPlayerDesignUITests/testLandscapeLEDConsumesRightHalf が成功し、新しい入力2ケースは opt-in 未指定で意図どおり skip した。Device Hub では曲のタップ再生、一時停止、停止、フィルターの開閉も確認し、元の All Videos / 停止状態へ戻した。修正後の画面は .build-logs/duo-toolbar-fixed.png に保存した。

縦向きの半折り再生画面で中央の空白を追加しない(2026年10月15日)

ユーザーの指定により、展開したDuoを縦向きで持つ場合は、半折り(Book / Partially Open)でも再生画面の中央にdivision由来の空白を追加しない。Openと同様に、現在のViewの上半分をLEDとメーター、下半分を操作部に割り当てる。横向きの再生パネルとライブラリのNavigationSplitViewによる標準の適応は別の経路であり、今回の変更対象は縦向きの再生画面。

従来は DuoPortraitPlayerPlacement が横方向のactive divisionの上下端から表示部・操作部の端を決め、さらに reservedFrames からdivisionを差し引いていた。縦向きの配置計算にはocclusionだけを渡すようにし、上下の境界をViewの midY に固定した。共通の SeekBarView にも独立した予約領域回避があるため、縦向きの再生画面からだけdivision回避を無効にする。カメラなどのactive occlusionは両経路で引き続き避ける。回転・compactへの切り替えでは既存の別Viewと既定の回避方針が適用される。プレイヤー、キュー、再生世代、シートの所有は変更しない。

確認した呼び出し元は、通常の縦向き画面を提示する MainView.expandedIOSView と、ClosedからNow Playingを開いて展開した場合の IPhoneDeckView.expandedDisplay。両方とも同じ DuoPortraitPlayerView を使用する。後者の実際のClosed→Bookの操作は今回再実行していない。

検証結果:

  • 既存の80pt幅の横方向division fixtureを使い、HEADの旧配置計算と変更後の配置計算を同じSwiftスクリプトで比較。表示部と操作部の中央の隙間は旧処理80pt、変更後0pt。これは配置計算の比較であり、旧アプリを実画面で再実行した結果ではない。
  • Xcode 27.1 RC / Duo iOS 27.1で、DuoPortraitPlayerLayoutTests と AdaptiveBottomPanelTests の17件が成功。中心の連続性、原点と高さの異なるView、上下両方のカメラ回避、範囲外のocclusion、および既存の横向きdivision回避を確認。
  • Device Hubで対象DuoをPartially Openにして縦向きへ実際に回転。診断値は portrait: true、hasActiveDivision: true、Viewの寸法669×835pt、divisionのframeはx=0、y=373.5、width=669、height=40pt(top/bottom margins各20pt)。この状態で testPartiallyFoldedPortraitDoesNotReserveACenterGap と testPortraitVisualsStayAboveControlsAndLibraryReturns の2件がskipなしで成功。表示部と操作部がViewの中央で接すること、シークバーの存在とhittability、44ptの再生操作、Library・Settingsの開閉と再生継続を確認。
  • SwiftLint 0.65.1をリポジトリの設定で実行。全体は既存の38警告・エラー0、変更したSwiftファイルだけのlintは警告・エラー0。ベースラインは変更していない。git diff --check も成功。

全ログはアプリ側の .build-logs/duo-portrait-ios-20261015-112129-52337.log と .build-logs/duo-portrait-ui-20261015-112403-54114.log に保存した。最大アクセシビリティ文字サイズ、active cameraが画面内を覆う状態の実UI、Open↔Bookの連続操作、Closedからの展開、iPadの実UIは今回再検証していない。occlusionは単体テストで確認した範囲と区別する。

補足:Appleのframe仕様では ReservedRegion.frame 自体がmarginsを含む。既存の DeckReservedRegions.frames はさらにmarginsを加算するため、非ゼロのmarginsで回避範囲が広がる別の課題がある。今回の変更は縦向きの再生画面でdivisionを除外する範囲に限定し、共通の余白計算や横向きの挙動は変更していない。

macOS Debugビルドも成功(.build-logs/duo-portrait-mac-20261015-112540-54376.log)。既存のlint・Info.plist警告は残るがエラー0。最終lintログは .build-logs/duo-portrait-lint-20261015-112736-56582.log。ユーザーへの対象確認でも「縦向きの再生画面」が明示され、この範囲で確定した。

縦向き・半折りのフルスクリーン動画を上側へ配置(2026年10月15日)

フルスクリーンの PlayerLayerView が画面全体を占有し、AVPlayerLayerの resizeAspect が画面全体の中央に動画を置いていたため、半折りの縦向きで映像が折り目をまたいでいた。FullScreenVideoView 内で現在のローカル座標におけるactive divisionを読み、縦長の画面を横切る分割領域がある場合だけ、その上端までを動画領域にする。Openや横向きでは全画面へ戻す。画面寸法や端末名は固定せず、同じAVPlayerとPlayerLayerViewのサイズを更新する。再読み込みや再生世代の変更は行わない。

調査した提示経路は VideoAreaView のフルスクリーンボタン/ダブルタップ、IOSLibraryPresentations、IPhoneDeckView、macOSの VideoFullScreenPresenter。共通のFullScreenVideoViewを修正し、macOSの描画経路は維持した。iOS 27.1 APIはiOS内でavailability guardを通し、27.0では分割領域を空配列として扱う。再生・閉じるボタンの既存の安全領域回避も維持する。

Appleのframe仕様はmarginsを含む矩形を返す。そのため今回の動画配置は reservedRegions(kind: .division, layoutDirectionBehavior: .fixed) のactiveなframeを直接使用し、marginsを再加算しない。既存の DeckReservedRegions.activeDivisionFrames は追加の余白を含む別の計算であり、呼び出し元全体の変更は今回の修正に含めない。

検証用動画はUIテスト内で320×180、60秒の赤いH.264動画を生成し、base64でDEBUGの電話レイアウトfixtureへ渡す。既存のautoplay probeで動画を開始し、ライブラリのフィルターや行選択と分けてフルスクリーン配置を検証する。UIテストは実際のportrait・active divisionとスクリーン座標の一致を前提にし、不一致ならskipする。XCTestのsynthetic rotationで姿勢を代用しない。動画のGeometryReader診断は fullScreenVideoMetrics で読める。

  • FullScreenVideoLayoutTests は上側領域、原点の異なる矩形、画面幅を超えるdivision、Openへの復帰、横向き、範囲外・画面端・縦方向・部分的・高さゼロのdivision、複数divisionの順序を確認する。
  • DuoFullScreenVideoUITests/testPartiallyFoldedPortraitVideoStaysAboveTheFold は実際のdivision上端と動画配置の一致、一時停止/再開、Close、再提示後のダブルタップで閉じる操作を確認する。
  • Device Hubの直接操作でも半折りの上側表示と、一時停止中にOpenへ切り替えた際の中央表示を目視確認した。動作中のOpen↔Book遷移における時間・item ID・generationの連続性、横向きへの実際の回転、Closedからの展開、VoiceOver実機操作は今回未検証。状態を保つ実装と、連続性の実測は区別する。

最初のUIテストはAll Songs内から動画を探して失敗した。All Videosへの移動後もタップによる再生開始を安定して検証できなかったため、フルスクリーン配置のテストでは既存のautoplay probeで開始条件をそろえた。動画ライブラリのタップ動作をこのテストの成功範囲に含めない。

検証ログ:

  • Xcode 27.1 RC、Duo iOS 27.1:.build-logs/duo-video-verified-20261015-120231-68632.logで配置6件+半折りフルスクリーンUI1件の計7件がskipなしで成功。
  • iPad(A16)、iOS 27.0:配置6件と IPadLayoutUITests/testMainScreenAndSettingsKeepTabletLayout の計7件が成功。27.1 APIのfallbackで起動し、既存のメイン画面・Settings操作を確認。.build-logs/duo-video-ipad-20261015-120032-68007.log。
  • macOS Debugビルド成功。.build-logs/duo-video-final-macos-20261015-120029-67988.log。最終のiOS限定のframe取得変更はmacOS分岐に影響しない。
  • scripts/lint.sh --quiet はエラー0、既存38警告で終了コード0。変更したSwiftファイルの新規lint警告なし。.build-logs/duo-video-lint.log。ベースラインは変更していない。git diff --check も成功。

ReservedRegion のマージンの二重加算を解消(2026年10月15日)

ReservedRegion.frame は margins を含む矩形である。Apple の frame 仕様と、iOS 27.1 SDK(Xcode 27.1 RC)の UIViewReservedRegion.h の説明(frame は "including the margins"、margins は "included in the frame")で確認した。2026-10-04 のコミット 8baed34 で導入した DeckReservedRegions.frames は frame をさらに margins の分だけ広げていた。そのため、margins が 0 でない division では回避範囲が上下または左右に 20 pt ずつ余分に広がっていた。UI テストの DuoPlayerDesignUITests.activeRegions も同じ計算をしており、アプリとテストが同じ誤りを前提に一致していた。

修正では、active な region の frame をそのまま返す。PR #86 で FullScreenVideoView に追加した独自の division 取得は、同じ値を返すようになった DeckReservedRegions.activeDivisionFrames に戻した。UI テストの helper も margins を加算しない。

呼び出し元と配置の変化

呼び出し元 経路 変化
DuoLandscapePlayerView 展開した Duo の横向き再生画面 Book 横向きの実測値(ルート 867 pt、division x 455.5 pt・幅 40 pt)で計算すると、操作部の幅は 435.5→455.5 pt、LED は x 515.5→495.5 pt から始まり幅 351.5→371.5 pt。両方が division の端に接する
AdaptiveBottomPanelView 共通のデッキ下部パネル 同じ AdaptiveBottomPanelPlacement.make を使うため、縦方向 division の左右の余分な 20 pt がなくなる。controlWidthForVerticalDivision の幅も同じ分広がる
SeekBarView、DeckSafeControlsOverlay、DeckSafeContentRegion シーク、全画面デッキの操作・内容 division/occlusion の回避範囲が frame と一致する
IPhoneDeckArrangementView.shouldArrange 全画面デッキの配置切替 division と View の交差判定が frame で行われる。View の端から 20 pt 以内に margins 分だけ広げた矩形が掛かる場合にのみ結果が変わる
DuoPortraitPlayerView 縦向き再生画面 PR #86 以降は occlusion だけを渡し、division は使わない。半折り縦向きの配置は変わらない
FullScreenVideoView フルスクリーン動画 以前から frame を直接使っていたため、値は変わらない

なお、上記「今回の確認結果」の Book・Portrait の行にある「表示部は y 353.5 pt まで、操作部は y 433.5 pt から」は、二重加算した 80 pt の division による配置だった(frame は y 373.5〜413.5 pt)。この縦向きの配置は PR #86 で division を使わない方式に置き換わっている。

検証結果

  • 単体テストの division fixture を Duo Simulator の実測値(Book 横向きの幅 40 pt)に合わせた。verticalDivisionHeightConvergesUsingTheLocalControlWidth は操作部と LED の幅が各 455.5 pt で、両方が division の端に接することを確認する。旧 fixture の幅 80 pt は二重加算後の値だった。
  • macOS:Xcode 27.1 RC で SimpleMediaPlayerTests の 827 件・132 suites が成功(.build-logs/margin-macos-test-20261015-165310-24099.log)。
  • iOS:Duo iOS 27.1 Simulator 向け build-for-testing が成功(.build-logs/margin-ios-build-20261015-165611-26383.log)。同じ Simulator で AdaptiveBottomPanelTests、DuoPortraitPlayerLayoutTests、FullScreenVideoLayoutTests の 23 件が成功(.build-logs/margin-duo-unit-20261015-165642-26654.log)。
  • scripts/lint.sh --quiet:既存の 38 警告のみ、エラー 0。変更ファイルの違反なし。ベースラインは変更していない。git diff --check も成功。
  • 半折り横向きの UI テスト testPartiallyFoldedLandscapePanelsMeetTheDivisionFrame を追加した。実際の division frame の端に操作部の右端と LED の左端が接すること、44 pt の再生操作が操作部に収まることを確認する。Device Hub で Partially Open・横向きを選ばない場合は skip する。

Device Hub での実画面の確認

ユーザーが Device Hub で Duo を Partially Open にし、横向き、続いて縦向きへ実際に回転させてから実行した。XCTest の合成回転は使っていない。

  • 横向き:testPartiallyFoldedLandscapePanelsMeetTheDivisionFrame が skip なしで成功(.build-logs/margin-duo-landscape-ui2-20261015-170244-28974.log、.build/margin-landscape2-170244.xcresult)。診断値はルート 867 × 635 pt(global 原点 0, 0)、safe area の trailing 84 pt・bottom 34 pt、active division は x 455.5・y 0・幅 40・高さ 669 pt(左右 margins 各 20 pt)。操作部は x 0〜455.5 pt(幅 455.5 pt)、LED は x 495.5〜867 pt(幅 371.5 pt)で、ともに y 419 pt・高さ 216 pt。両方が division の端に接し、修正前の計算値(操作部 435.5 pt、LED は x 515.5 pt から)にあった左右 20 pt ずつの余分な隙間がなくなった。active な occlusion(x 867・幅 84 pt)はルートの外にある。再生は同じ項目 ID・queue・generation 1 で、playing true、左右 RMS 約 0.575 を保った。
  • 当初の横向きケースは縦向き用の sceneContentFrame(ルートと画面の座標範囲の一致)を前提にしていたため skip した。xcsift の要約はこの skip も passed として数えるため、ログの Test Case ... skipped を確認する必要がある。DEBUG の duoLayoutMetrics にルートの globalX/globalY を加え、division をその原点で画面座標へ変換するよう改めた。
  • testLandscapeLEDConsumesRightHalf は Open 横向き専用で、active division があるため想定どおり skip した。
  • 縦向き:testPartiallyFoldedPortraitDoesNotReserveACenterGap、testPortraitVisualsStayAboveControlsAndLibraryReturns、DuoFullScreenVideoUITests/testPartiallyFoldedPortraitVideoStaysAboveTheFold の 3 件が skip なしで成功(.build-logs/margin-duo-portrait-ui-20261015-170808-30595.log、.build/margin-portrait-170808.xcresult)。診断値はルート 669 × 835 pt(global 原点 0, 82)、active division は x 0・y 373.5・幅 669・高さ 40 pt(上下 margins 各 20 pt)。再生画面は division を使わずルートの中央で上下に分かれ、フルスクリーン動画は frame の上端(y 373.5 pt)までに収まる。active な occlusion(y −82 pt・高さ 82 pt)はルートの外にある。
  • XCUIApplication.screenshot() の添付は展開画面では黒い画像だった。配置の確認は診断値に基づき、画像による目視確認は含まない。

未検証の条件

  • 今回測定した active な occlusion の margins はすべて 0 だった。margins が 0 でない occlusion の実測値はなく、その回避は単体テストの矩形計算の範囲にとどまる。
  • 全画面デッキ(Closed から Now Playing を開いて展開)の DeckSafeControlsOverlay と shouldArrange の実画面での変化は確認していない。
⚠️ **GitHub.com Fallback** ⚠️