Xcode 26.3 marked the first time Apple opened its own capabilities to external agents through MCP. Many of those capabilities were already reachable through the CLI, but Preview rendering was entirely new. Xcode 27 went a step further with a headless MCP server, making these tools even easier to call, so I wove Preview rendering deeply into my AI workflows. Along the way I ran into a few pitfalls, and came away with some reflections and hopes.
Previews Can’t Use the Right Rendering Device
In Xcode, the device a preview uses isn’t necessarily the simulator or physical device you’ve selected; developers can pick a device for the preview separately.
Take my MacBook as an example: because I have both Xcode 27.0 and 27.1 beta installed, even in 27.0 the preview device defaults to iPhone Duo. Switch to another device or platform manually and the preview follows — but only within Xcode’s traditional interactive UI.
Once you switch to the rendering tool provided by Xcode MCP, you’ll find there’s simply no way to specify the rendering device — not even from Xcode’s built-in AI chat.
The reason is that the MCP rendering tool currently has no parameter for the target device, and the renderedDestination in its result can’t be trusted either — it reports the simulator hosting the render, not the device model the preview actually emulates.
The #Preview macro likewise has no parameter for choosing a device, and it silently ignores .previewDevice.
So for now, the only way I can render on a specific device and get correct results is to fall back to the deprecated PreviewProvider:
@available(*, deprecated, message: "Capture preview: only PreviewProvider can specify a device")
struct CapturePreview_home: PreviewProvider {
static var previews: some View {
HomeScreens.view(for: "home")
.previewDevice(PreviewDevice(rawValue: "iPhone 18 Pro"))
.previewDisplayName("home")
}
}
Marking the type itself as deprecated with
@availablesilences the compiler warnings that come from using deprecated APIs.
The trade-off is that giving up the #Preview macro also means giving up many of the newer preview features. If you need previews for CI/CD or AI workflows, it’s best to keep a separate set of preview code just for those scenarios.
Update: After this article was published, erin sparling shared another workaround: by using
XcodeSwitchRunDestinationto switch the project’s active run destination before rendering, a regular#Previewcan render across different device families, such as iPhone and iPad.previewVariantOverridescan also be used to control orientation. However, this approach cannot reliably target a specific device model—for example, selecting an iPhone 17 Pro may result in the preview being rendered on an iPhone 18 Pro—and the full workflow depends on an.xcodeprojor.xcworkspace. For scenarios that require precise control over the Preview device or direct rendering from a Swift Package, thePreviewProviderapproach described in this article remains an option.
The View Code Changed, but the Screenshot Didn’t
An even more frustrating problem in automated scenarios: the view code has clearly changed, yet the Preview rendering tool still returns the old image — with no error at all.
Often the preview code and the view code don’t live in the same file. From what I’ve observed, if you only change other files that the previewed file depends on, Xcode sometimes skips the rebuild and simply hands back the previous result. Updating the file’s modification time (touch) doesn’t help; only a real change to the contents of the previewed file reliably triggers a rebuild.
The problem is especially pronounced when rendering views directly from an SPM package — in my tests it happened 20%–40% of the time. With an Xcode project (.xcodeproj) the rate is much lower — it didn’t happen again in my tests — but that doesn’t mean it never will.
Of course, an agent can compare the hash of the screenshot, or even how long the render took, to judge whether it got a fresh image. But this is only indirect evidence; they can’t guarantee that the returned screenshot is the one the agent actually asked for.
Fortunately, every time I take a screenshot I also collect other information from the view (each .designAnchor’s id, instance, label, and its window-coordinate frame), so I simply put a fingerprint into that information as well. The process looks like this:
- The tool computes a fingerprint from the source contents
- It modifies the Preview code to embed the fingerprint (this changes the contents of the previewed file, which by itself forces Xcode to rebuild)
- It retrieves the screenshot along with the corresponding information (JSON)
- It compares the fingerprint returned in the JSON with the one it computed; if they differ, it modifies the Preview file once more and re-renders, and if they still differ, the screenshot is discarded
fileprivate enum SourceFingerprint_8848880ac81b6df2 {}
struct CapturePreview_home: PreviewProvider {
static var previews: some View {
HomeScreens.view(for: "home")
/// Embed the fingerprint
.designProbe("home", build: String(describing: SourceFingerprint_8848880ac81b6df2.self))
.previewDevice(PreviewDevice(rawValue: "iPhone 18 Pro"))
}
}
Note that the fingerprint is a type name, not a string literal. Previews hot-swap literals, so changing only a literal may not trigger a rebuild at all, producing the illusion of “a new fingerprint on top of old code”; a type name, on the other hand, only changes when the code is recompiled.
You can view the code for .designProbe here.
This way, every screenshot I get is confirmed to come from the current code, and a stale one can never slip through again. On top of that, the anchor information lets me give much more targeted feedback within my AI workflows.
Other Pitfalls I Ran Into
A few other issues also come up often when using the Preview rendering tool:
- The preview host process crashes after code changes: the crash stack stops in SwiftUICore’s
DebugReplaceableViewChild.updateValue()with a failed type cast, during Preview’s hot replacement of a view implementation. How often it happens correlates strongly with whether the Preview file was rewritten — at worst, close to half the time. Fortunately, retrying once right away usually recovers. - Single-line
#Previewthrow off the index: when three#Previewin one file are each written on a single line,previewDefinitionIndexInFile: 2renders the first one (index 0), even though the returnedsourceLineNumberis correct. Writing them across multiple lines works perfectly. The takeaway is simple: always write#Previewacross multiple lines. - Files may not be found after adding or deleting them: rendering right after writing a new file reports
FileNotFoundError; rendering an untouched file right after deleting another reportsProviderError: noPreviewInfos. Sometimes waiting a second or two is enough, but headless mode caches the project structure, so newly added files often won’t be recognized until you close and reopen the workspace.
For all its pitfalls, the Preview rendering tool in Xcode still brings developers and creators enormous convenience, and opens up a lot of room for imagination.
AI + Preview + SwiftUI == A Creative Tool for Apple Developers
Our needs for UI differ at different stages of development. In the ideation stage, for example, I like to capture and organize ideas in documents — something AI can already help with a great deal. But whether those documents are turned into flowcharts or state analyses, the result still isn’t very intuitive for many developers.
So I brought Preview rendering into this stage. The UI here doesn’t need to be precise, and there’s no need to worry about runtime logic, but once the corresponding screenshots are embedded into the flow analysis, I can work through things much faster, and problems I hadn’t noticed or cases I hadn’t thought through surface much more easily.
The video below shows FlowCanvas, a tool I built myself. It turns documents into a visual, interactive canvas, and keeps it in sync with the documents after every adjustment.
SwiftUI Preview is also an excellent creative tool in its own right. When you have no idea where to take a UI design, you can let an agent explore divergent options for you, then iterate through feedback and conversation until you find the presentation you want.
The video below shows FormStudio, another tool I built on top of Preview rendering:
While building and using these tools, I kept thinking: how great would it be if Xcode offered capabilities like these directly? Even just opening up more plugin interfaces (independent of the CLI or MCP) so developers could extend it themselves would be a good option.
In the age of AI, the IDE’s presence is fading, but work related to interaction and design still leaves room for human developers.
In the UIKit era, Xcode offered Storyboard, an excellent tool that tried to bridge design and logic. Yet in the era of SwiftUI and AI, all we have is a fragmented set of Previews, offering nothing more than a keyhole view of the bigger picture. It’s time for Xcode to change — it shouldn’t serve only those who write code. AI is rapidly erasing the lines between programmers, designers, and product managers.
When Xcode is no longer bound by code, it can still win everyone’s favor.