iPhone Duo シミュレーターを初めて起動したのは 10 月 13 日、日記アプリ Lanternly のリリース準備をしていたときです。シミュレーター自体は役に立ちました。そこまでたどり着くのが大変でした。通常の Xcode 27.0 には Duo の端末がそもそもありません。ランタイムは進捗を一行も出さずにダウンロードされます。Simulator は姿を消して DeviceHub に置き換わり、必要な操作の半分はコマンドラインからできません。
この記事には、その途中で役立ったことをまとめました。シミュレーターまでのたどり着き方、2つの画面のキャプチャ、スクリプトからの折りたたみと回転、App Store 用の iPhone Duo スクリーンショットの撮り方です。アプリ側で何を直す必要があったかは、iPhone Duo 対応で起きた問題の記事にまとめています。
iPhone Duo シミュレーターはどこにあるか#
Duo の端末は Xcode 27.1 にしかありません。私は Release Candidate(ビルド 27A9275)をリリース版 27.0 と置き換えずに並べて入れ、DEVELOPER_DIR で使う Xcode を選んでいます。
export DEVELOPER_DIR=~/Downloads/Xcode.app/Contents/Developer # 27.1 RC
xcrun simctl list devicetypes | grep -i duo
#> iPhone Duo (com.apple.CoreSimulator.SimDeviceType.iPhone-Duo)Xcode 27.0 では同じコマンドで何も見つかりません。検索候補には「iPhone Duo simulator missing」が出てきますが、最初の答えはこれです。端末の種類はランタイムではなく Xcode 27.1 と一緒に入ります。
2つ目の答えは iOS 27.1 ランタイムです。これがないと、端末はリストに出ても作成できません。
xcodebuild -downloadPlatform iOS
#> Downloading iOS 27.1 Simulator (24A94232) (arm64): 22,0% (1,73 GB of 7,85 GB)サイズは 7.85 GB です。最初は | tail -20 を付けて実行したため、固まっているように見えました。tail はコマンドが終わるまで何も出力しないからです。出力をファイルに書き出すと、普通に進捗が見えました。インストールの確認はこうです。
xcrun simctl runtime list | grep 27.1
#> iOS 27.1 (24A94232) - … (Ready)端末を作って2つの画面を見つける#
UDID=$(xcrun simctl create "iPhone Duo" \
com.apple.CoreSimulator.SimDeviceType.iPhone-Duo \
com.apple.CoreSimulator.SimRuntime.iOS-27-1)
xcrun simctl boot "$UDID"
xcrun simctl io "$UDID" enumerateenumerate は長いポート一覧を出します。必要なのは種類が Integrated の2つの画面です。
| 画面 | --display の名前 | ピクセルサイズ |
|---|---|---|
| 外側(折りたたみ) | primary | 1398 × 2034 |
| 内側(開いた状態) | primary-1 | 2007 × 2853 |
これは App Store Connect の iPhone Duo スクリーンショット枠とまったく同じサイズです。画面は1つずつキャプチャします。
xcrun simctl io "$UDID" screenshot --display=primary outer.png
xcrun simctl io "$UDID" screenshot --display=primary-1 inner.png起動直後の端末は折りたたまれているので、内側の画面は黒い四角として撮れます。エラーではありません。
Simulator に代わる DeviceHub#
Xcode 27 では Simulator アプリが DeviceHub(Xcode.app/Contents/Applications/DeviceHub.app)に置き換わりました。ウィンドウ下部に、回転ボタンと3つの姿勢(折りたたみ、ブック、フラット)のボタンが並んでいます。Duo の姿勢を変えられるのはここだけです。
simctl に期待して確かめましたが、回転や折りたたみのコマンドはありません。あるのは、画面を1つだけ消せる io screenConfig です。
xcrun simctl io "$UDID" screenConfig --display=primary power off
#> Screen 1 power state set to offこれでは姿勢は変わりません。外側の画面が消えるだけで、端末は折りたたまれたままなので内側も黒いままです。
スクリプトから DeviceHub のボタンを押す#
次に AppleScript での操作を試しましたが、だめでした。System Events はプロセスを認識するものの、ウィンドウは 0 個、メニューバーも取得できません。同じセッションで Finder は普通に読めます。うまくいったのは CoreGraphics のマウスイベントでした。10 行ほどの Swift ツールです。
import CoreGraphics
import Foundation
// click x y — 画面上の点をクリック(座標はポイント単位)
let a = CommandLine.arguments
let p = CGPoint(x: Double(a[1])!, y: Double(a[2])!)
let src = CGEventSource(stateID: .hidSystemState)
CGEvent(mouseEventSource: src, mouseType: .mouseMoved, mouseCursorPosition: p, mouseButton: .left)?.post(tap: .cghidEventTap)
usleep(150_000)
CGEvent(mouseEventSource: src, mouseType: .leftMouseDown, mouseCursorPosition: p, mouseButton: .left)?.post(tap: .cghidEventTap)
usleep(80_000)
CGEvent(mouseEventSource: src, mouseType: .leftMouseUp, mouseCursorPosition: p, mouseButton: .left)?.post(tap: .cghidEventTap)swiftc -O click.swift -o click でビルドします。座標は screencapture -x の画像から取ります。Retina 画面ではピクセル単位なので、2 で割ってポイントにします。
時間を取られた細かい点です。
- クリックの前にウィンドウを前面に出し(
tell application "DeviceHub" to activate)、1〜2 秒待ちます。待たないと最初のクリックが空振りすることがありました。 - 縦向きでは姿勢アイコンが回転して描かれます。ボタンの順番は同じですが、見分けにくくなります。
- 本体が回転していると、折りたたんだあと外側の画面が横向きになります。
--display=primaryのキャプチャの幅が 1398 になるまで回転ボタンを押します。 - DeviceHub のメニューバーも同じクリックで操作できます。
Controls → Home、Device → Touch ID → Enrolledなどです。System Events から送ったキーボードショートカットは届きませんでした。
DeviceHub を使わない回転#
iPhone と iPad では、アプリ側から回転させるほうが楽です。シーンに向きを要求するデバッグ用の起動フラグを追加しました。
#if DEBUG
// -orientation landscape,portrait — 起動後 4 秒ごとに1ステップ
static func applyOrientationFlag() {
let args = CommandLine.arguments
guard let i = args.firstIndex(of: "-orientation"), i + 1 < args.count else { return }
for (step, name) in args[i + 1].split(separator: ",").enumerated() {
let mask: UIInterfaceOrientationMask = name == "portrait" ? .portrait : .landscapeRight
DispatchQueue.main.asyncAfter(deadline: .now() + Double(step + 1) * 4) {
for scene in UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }) {
scene.requestGeometryUpdate(.iOS(interfaceOrientations: mask))
}
}
}
}
#endifxcrun simctl launch "$UDID" <bundle> -orientation landscape,portrait で画面が行き来し、各ステップの間のキャプチャで、幅が変わったときのアプリの挙動が確認できます。iPhone 18 Pro Max ではうまくいきました。iPadOS 27 の iPad ではアプリがウィンドウ内で動くため、システムは回転の要求を無視します。
App Store 用の iPhone Duo スクリーンショット#
枠のサイズが画面バッファと一致するので、元画像は simctl io screenshot からそのまま取れます。撮影前に端末を整えます。
xcrun simctl status_bar "$UDID" override --time 9:41 --batteryLevel 100 \
--batteryState discharging --wifiBars 3 --cellularBars 4
xcrun simctl privacy "$UDID" grant microphone <bundle>古いチートシートにある discharged は simctl に拒否されます。使えるのは charging、charged、discharging です。
完成した画像を見て初めて気づいたことが3つあります。
- システムのダイアログ。 アプリを入れ直したあと、音声録音の画面でマイクへのアクセス許可を求めるダイアログが出ました。
simctl privacy … grantで消せます。 - Touch ID。 メニューで
Device → Touch ID → Enrolledにチェックを入れるまで、設定画面には生体認証が使えないと正直に表示されます。言語を変えるときなどにシミュレーターを再起動すると、このチェックは外れます。 - デバッグ用の表示。 私のデバッグビルドでは、チャットのヘッダーに応答元(「· simulator」)、設定にローカル保存モードが表示されます。起動フラグ
-storeShotsで撮影中だけ隠しています。リリースビルドは変わりません。
そして一番大事なルールです。一度これを間違えました。端末の枠には、その端末で撮った画像だけを入れます。Duo のシミュレーターがまだなかった頃、iPad の画像を Duo のサイズに収めてしまい、削除することになりました。スクリーンショットは、その人が自分の画面で見るものを約束します。Duo で見えるのは別のレイアウトです。App Store のほかの素材のサイズと形式は Creative Assets ガイドにまとめています。
うまくいかなかったこと#
ジェスチャーでの Split View。 Duo の内側の画面では、iPad と同じく 2 つ目のアプリを Dock からドラッグします。CGEvent でのマウススワイプは、長いと App スイッチャーが開き、短いと何も起きませんでした。このケースは手動での確認が残っています。画面の狭い半分での動きは、外側の画面でもともと狭いウィンドウで動いていることで部分的にはカバーされていますが、それは推論であってテストではありません。
コマンドラインからの姿勢変更。 折りたたみ、ブック、フラットは DeviceHub のボタンでしか切り替えられません。CI をシミュレーターで回している場合、姿勢のテストはまだそこに移せません。
ゼロから Duo をテストするなら、順番はこうです。メインの Xcode の隣に Xcode 27.1、出力をファイルに書き出しながら 27.1 ランタイム、simctl create と enumerate、姿勢には DeviceHub と小さなクリックツール。これらの画面でアプリの何が壊れ、どう直したかは対応の記事に、端末の全体的な考え方は iPhone Duo ガイドにあります。



