Workarounds for "Letting AI See SwiftUI": device and orientation in RenderPreview
Notes on the open problems in fatbobman's post, from driving Xcode's MCP bridge (xcrun mcpbridge) from an external agent. Observed on Xcode 27.0 RC, with plain #Preview (no PreviewProvider).
Orientation: solved with previewVariantOverrides
Every RenderPreview result includes a supportedPreviewVariantOverrides field listing the variants that preview accepts. Pass one back as previewVariantOverrides and the render uses it:
{
"workspaceIdentifier": "<id>",
"sourceFilePath": "App/Views/SomeView.swift",
"previewDefinitionIndexInFile": 0,
"previewVariantOverrides": { "Orientation": "Landscape Left" }
}
Read the keys and values from supportedPreviewVariantOverrides rather than hard-coding them. We use this for every landscape iPad render.
Device: switch the run destination before rendering
RenderPreview has no device parameter, and .previewDevice is ignored under #Preview. But the preview renders on the workspace's active run destination, and the bridge can change that:
// XcodeSwitchRunDestination
{ "workspaceIdentifier": "<id>", "displayTitle": "iPhone 17 Pro (27.0)" }
Check the switch against its own response. It returns activeDestinationDisplayTitle, so compare that to what you asked for, and stop if they differ.
Wait before the first render. We give it about 45 s after a switch; rendering immediately can still use the old destination.
XcodeListRunDestinations hung for 240 s, twice, on this Xcode RC, so don't depend on listing. XcodeSwitchRunDestination takes the display title directly.
This reliably selects iPhone vs iPad with #Preview, so you don't need the deprecated PreviewProvider for that.
The loop, in short
XcodeOpenWorkspace, then XcodeSwitchRunDestination. Check activeDestinationDisplayTitle and wait.
For each preview, call RenderPreview with previewDefinitionIndexInFile, plus previewVariantOverrides if you need an orientation. If the response says "Waiting for packages", retry after 60 s.
Copy previewSnapshotPath out, and check that renderedDestination is the right device family.
AGENTS.md: rendering SwiftUI previews through Xcode's MCP bridge
Instructions for an agent that needs to see a SwiftUI view: render a #Preview to a PNG on a chosen device family and orientation, and confirm it's the right render before trusting it. Written against Xcode 27.0 RC. Works with plain #Preview; no PreviewProvider needed.
Connect
Start the bridge and speak MCP (JSON-RPC 2.0, one JSON object per line) over stdio:
xcrun mcpbridge
Send initialize (e.g. protocolVersion: "2025-06-18"), wait for its response, then send notifications/initialized.
Call tools with tools/call. The result text is in result.content[0].text, and it's JSON: parse it.
Xcode must allow external agents (Settings: "Allow external coding agents"). Depending on the Xcode build, the first connection may show an approval dialog.
Procedure
- Open the workspace
XcodeOpenWorkspace with { "path": "<abs path to .xcodeproj or .xcworkspace>" }. Keep the returned workspaceIdentifier; every later call needs it.
- Choose the device: switch the run destination
RenderPreview has no device parameter, and #Preview ignores .previewDevice. Previews render on the workspace's active run destination, so set that first:
XcodeSwitchRunDestination with { "workspaceIdentifier": "<id>", "displayTitle": "iPhone 17 Pro (27.0)" }
Check the switch. Parse activeDestinationDisplayTitle from the response. If it isn't exactly what you asked for, stop. Never render on an unconfirmed destination.
Wait about 45 s before the first render. Rendering immediately after a switch can still use the previous destination.
Don't depend on XcodeListRunDestinations. It hung for 240 s, twice, on Xcode 27.0 RC. XcodeSwitchRunDestination accepts the display title directly, so you don't need to list first.
- Render
RenderPreview with:
{
"workspaceIdentifier": "<id>",
"sourceFilePath": "<path to the .swift file>",
"previewDefinitionIndexInFile": 0,
"timeout": 900,
"previewVariantOverrides": { "Orientation": "Landscape Left" }
}
previewDefinitionIndexInFile is the 0-based index of the #Preview within that file.
Orientation (and other variants): omit previewVariantOverrides on the first render. The response lists supportedPreviewVariantOverrides; take the keys and values from there and pass them back. Don't hard-code values you haven't seen in that list.
If the response text contains "Waiting for packages", wait 60 s and retry, up to about 4 times.
- Verify the render before using it
From the parsed result:
previewSnapshotPath: the PNG. Copy it somewhere stable right away.
errors: must be empty.
renderedDestination.deviceModelName / .systemVersion: use these as a guard, not as a record of what you asked for. Check the family ("iPhone" vs "iPad"). If it's wrong, stop and discard the image.
The exact model is not controllable. Asking for "iPhone 17 Pro (27.0)" has rendered as "iPhone 18 Pro": same size class, different model. Don't claim a specific model in your output.
Look at the PNG (open it with your image-reading tool) before describing it. A render can succeed and still show the wrong state.
Stale renders: a render can come from a cached build. If freshness matters, embed a fingerprint in the preview (e.g. a short hash or a counter drawn in the view) and check that it shows up in the image. That's the more robust approach described in the original post.
- Close
XcodeCloseWorkspace with { "workspaceIdentifier": "<id>" }, then end the bridge process.
Rules
One destination per run. To render iPhone and iPad, run the procedure twice, switching destinations between runs.
Name output files with the preview label, the device family and the source revision (e.g. footer-idle-iphone-<sha7>.png) so renders from different builds can't be mixed up.
Report what was verified: the destination the switch confirmed, the family the render reported, and whether the fingerprint matched. If any check failed, say so rather than presenting the image.
Preview fixtures that exist only in DEBUG must be wrapped, previews included, in #if DEBUG, or Release builds will fail to compile.