Problem
Previews compiled against Xcode build artifacts (.xcodeproj and .xcworkspace) silently fail to render TabView page contents at runtime. Accessibility structure is intact — the tree exposes the page indicator ("page 1 of 3") and correct frames — but the visual content of each page is missing. Severity differs by platform:
- iOS simulator: page area renders as an empty white rectangle. No gradient, no text, no page dots visible in the snapshot.
- macOS (NSHostingView): page area renders dimmed/translucent. Title and counts are faintly visible but the gradient background is mostly absent.
SPM and Bazel build paths render the same TabView correctly — full blue gradient, "Progress", "1/8", "7 remaining", visible page dots — both on macOS and iOS. So the bug is specific to how PreviewsMCP compiles preview dylibs against .swiftmodule / object files produced by xcodebuild.
Reproduction
Discovered while running the integration-test skill end-to-end after merging #150. Steps:
swift build at the repo root.
cd examples/xcodeproj && mint run xcodegen generate && xcodebuild build -project ToDo.xcodeproj -scheme ToDo -destination 'platform=macOS'.
- Call
preview_start on examples/xcodeproj/Sources/ToDo/ToDoView.swift with projectPath: examples/xcodeproj/. Snapshot the macOS session — Progress card area renders as a dimmed gray box.
- Call
preview_start again with platform: "ios" on the same file. Snapshot — Progress card area renders as a completely empty white rectangle.
- Repeat with
examples/xcworkspace/ (workspace build path) — same dimmed/empty rendering.
- Repeat with
examples/spm/ and examples/bazel/ — Progress card renders correctly with full gradient, text, page dots.
The accessibility structure is the same in all cases:
preview_elements (xcodeproj iOS):
...
{"frame":{"height":119,"width":361,"x":16,"y":150}, "role":"group",
"children":[{"frame":..., "traits":["adjustable","updatesFrequently"], "value":"page 1 of 3"}]}
...
So the view hierarchy is constructed and laid out, but the page closure body either runs into a swallowed runtime error or produces no rendered output.
Hypothesis
Possibilities (none verified):
- Symbol visibility / linkage divergence. Xcode emits
.swiftmodule and object files with default visibility/linkage that differs from swift build output. The TabView page closures, captured across module boundaries when the preview dylib loads, may resolve symbols differently.
- Resource bundle resolution. TabView's page-style indicators may pull from a SwiftUI resource bundle whose lookup path differs when the consuming dylib is built against
xcodebuild artifacts vs. SPM artifacts.
- Optimization-level mismatch. xcodebuild
Debug vs. swift build's default debug differ in inlining behavior; a @_alwaysEmitIntoClient or @inlinable boundary could silently strip page-content rendering when the consumer's optimization differs from the producer's.
- Closure capture across the bridge generator. Less likely given that other view contents (List rows, Toggles) render fine, but worth checking whether
TabView { ForEach... } patterns specifically are mishandled in the generated bridge code when the underlying types come from Xcode artifacts.
Prerequisite cleanup (do these first — they'll make the diagnosis tractable)
The current build system is invisible from progress messages and daemon logs. BuildHelpers.swift:detectAndBuild reports "Detecting project..." then "Building..." — neither names the detector that matched. BuildSystemDetector.detect (Sources/PreviewsCore/BuildSystem.swift:18-73) and the three BuildSystem implementations (SPMBuildSystem, BazelBuildSystem, XcodeBuildSystem) have zero Log.info / print / fputs calls. So when comparing an SPM run against an Xcode run today, you have to infer which detector fired from the projectPath argument or filesystem markers. That makes any otool/nm/swiftc-line diff guess-work.
1. Log the selected build system. One line right after BuildSystemDetector.detect returns, in BuildHelpers.detectAndBuild:
Log.info("buildSystem: \(type(of: buildSystem)) projectRoot=\(buildSystem.projectRoot.path)")
The Log.info line goes to ~/.previewsmcp/serve.log (UDS daemon) and the stdio MCP server's stderr. Tiny change. Bisect becomes trivial.
2. Document the artifact layout for each build system uniformly. docs/build-system-integration.md has a dedicated Artifact Layout section for SPM (lines 46–58) but no parallel sections for Xcode or Bazel. Bazel gets one line ("Find build outputs in bazel-bin/"); Xcode artifact layout appears only inside the comparison section "How Xcode Previews Does It," which describes Xcode's own internal behavior — not the layout PreviewsMCP consumes from xcodebuild. Add explicit "Artifact Layout" subsections for both:
- Xcode (
.xcodeproj / .xcworkspace): where exactly does PreviewsMCP look in DerivedData? Which Build/Intermediates.noindex/<Target>.build/<Configuration>-<Platform>/Objects-normal/<arch>/ paths does it read from? Which .swiftmodule flavor (.private.swiftinterface vs. .swiftmodule)? Where do object files come from (Objects-normal/<arch>/*.o)? What linker artifacts (<Target>.dylib, <Target>.framework)?
- Bazel: where in
bazel-bin/ does PreviewsMCP look for .swiftmodule and .o? How are external dependencies' artifacts located? Does it run bazel build itself, or rely on the user having built first?
The diff between these layouts is the most likely seat of this bug — a comprehensive layout doc would either point at the answer or rule out resource-bundle / search-path divergences.
Suggested approach: diagnose first, don't chase a fix
This is unlikely to be a small bug. The "items render but TabView pages don't" pattern points at something subtle in symbol or resource resolution across the dylib boundary. Plan to spend the first session on diagnosis, not patches:
- Land the prerequisite cleanup (detector logging + artifact-layout docs).
- Run the SPM repro and the xcodeproj repro back-to-back, capturing the bridge dylib path each session loads (visible in serve.log under
setup-dylib / dylib-load lines and now the new buildSystem: line).
otool -L <dylib> on both — diff the dependent libraries and rpaths. Differences in @rpath resolution or which SwiftUI variant is linked are load-bearing.
nm -gU <dylib> | grep -i tab (and the same for Page, IndexView) on both — confirm whether the page-style symbols differ in visibility, mangling, or presence at all.
- Diff the swiftc command lines PreviewsMCP generates for the two consumers (capture from
Compiler.swift invocation logs). Mismatched optimization levels, search paths, or -l flags will surface here.
- Only after the diff produces a concrete delta should a fix be attempted.
The integration-test skill already exercises both paths back-to-back, which makes capturing comparable diagnostic output cheap — restart Claude Code, run the skill against examples/spm/, capture, then against examples/xcodeproj/, capture.
Why this matters
xcodeproj and xcworkspace are first-class supported build systems in PreviewsMCP, and TabView is a common SwiftUI primitive (any paged onboarding flow, dashboard, etc., would hit this). Right now anyone using PreviewsMCP against a real Xcode project to preview a TabView-containing view would see blank page bodies and have no obvious diagnostic.
The integration-test skill already exercises this — examples/xcodeproj/Sources/ToDo/ToDoView.swift and examples/xcworkspace/Sources/ToDo/ToDoView.swift both contain a TabView. The README for each example claims "first summary card (blue 'Progress') should show '1/8' with '7 remaining'", which doesn't match observed behavior. Either the bug regressed silently, or the READMEs were aspirational from the start. git log -p examples/xcodeproj/README.md would clarify.
Out of scope
This is independent of #150 (the build-info staleness check that surfaced the bug). The two PRs touch disjoint code paths.
🤖 Filed from a Claude Code session running the integration-test skill end-to-end to validate PR #150.
Problem
Previews compiled against Xcode build artifacts (
.xcodeprojand.xcworkspace) silently fail to renderTabViewpage contents at runtime. Accessibility structure is intact — the tree exposes the page indicator ("page 1 of 3") and correct frames — but the visual content of each page is missing. Severity differs by platform:SPM and Bazel build paths render the same TabView correctly — full blue gradient, "Progress", "1/8", "7 remaining", visible page dots — both on macOS and iOS. So the bug is specific to how PreviewsMCP compiles preview dylibs against
.swiftmodule/ object files produced byxcodebuild.Reproduction
Discovered while running the
integration-testskill end-to-end after merging #150. Steps:swift buildat the repo root.cd examples/xcodeproj && mint run xcodegen generate && xcodebuild build -project ToDo.xcodeproj -scheme ToDo -destination 'platform=macOS'.preview_startonexamples/xcodeproj/Sources/ToDo/ToDoView.swiftwithprojectPath: examples/xcodeproj/. Snapshot the macOS session — Progress card area renders as a dimmed gray box.preview_startagain withplatform: "ios"on the same file. Snapshot — Progress card area renders as a completely empty white rectangle.examples/xcworkspace/(workspace build path) — same dimmed/empty rendering.examples/spm/andexamples/bazel/— Progress card renders correctly with full gradient, text, page dots.The accessibility structure is the same in all cases:
So the view hierarchy is constructed and laid out, but the page closure body either runs into a swallowed runtime error or produces no rendered output.
Hypothesis
Possibilities (none verified):
.swiftmoduleand object files with default visibility/linkage that differs fromswift buildoutput. The TabView page closures, captured across module boundaries when the preview dylib loads, may resolve symbols differently.xcodebuildartifacts vs. SPM artifacts.Debugvs.swift build's default debug differ in inlining behavior; a@_alwaysEmitIntoClientor@inlinableboundary could silently strip page-content rendering when the consumer's optimization differs from the producer's.TabView { ForEach... }patterns specifically are mishandled in the generated bridge code when the underlying types come from Xcode artifacts.Prerequisite cleanup (do these first — they'll make the diagnosis tractable)
The current build system is invisible from progress messages and daemon logs.
BuildHelpers.swift:detectAndBuildreports"Detecting project..."then"Building..."— neither names the detector that matched.BuildSystemDetector.detect(Sources/PreviewsCore/BuildSystem.swift:18-73) and the threeBuildSystemimplementations (SPMBuildSystem,BazelBuildSystem,XcodeBuildSystem) have zeroLog.info/print/fputscalls. So when comparing an SPM run against an Xcode run today, you have to infer which detector fired from theprojectPathargument or filesystem markers. That makes any otool/nm/swiftc-line diff guess-work.1. Log the selected build system. One line right after
BuildSystemDetector.detectreturns, inBuildHelpers.detectAndBuild:The
Log.infoline goes to~/.previewsmcp/serve.log(UDS daemon) and the stdio MCP server's stderr. Tiny change. Bisect becomes trivial.2. Document the artifact layout for each build system uniformly.
docs/build-system-integration.mdhas a dedicated Artifact Layout section for SPM (lines 46–58) but no parallel sections for Xcode or Bazel. Bazel gets one line ("Find build outputs inbazel-bin/"); Xcode artifact layout appears only inside the comparison section "How Xcode Previews Does It," which describes Xcode's own internal behavior — not the layout PreviewsMCP consumes fromxcodebuild. Add explicit "Artifact Layout" subsections for both:.xcodeproj/.xcworkspace): where exactly does PreviewsMCP look inDerivedData? WhichBuild/Intermediates.noindex/<Target>.build/<Configuration>-<Platform>/Objects-normal/<arch>/paths does it read from? Which.swiftmoduleflavor (.private.swiftinterfacevs..swiftmodule)? Where do object files come from (Objects-normal/<arch>/*.o)? What linker artifacts (<Target>.dylib,<Target>.framework)?bazel-bin/does PreviewsMCP look for.swiftmoduleand.o? How are external dependencies' artifacts located? Does it runbazel builditself, or rely on the user having built first?The diff between these layouts is the most likely seat of this bug — a comprehensive layout doc would either point at the answer or rule out resource-bundle / search-path divergences.
Suggested approach: diagnose first, don't chase a fix
This is unlikely to be a small bug. The "items render but TabView pages don't" pattern points at something subtle in symbol or resource resolution across the dylib boundary. Plan to spend the first session on diagnosis, not patches:
setup-dylib/ dylib-load lines and now the newbuildSystem:line).otool -L <dylib>on both — diff the dependent libraries and rpaths. Differences in@rpathresolution or which SwiftUI variant is linked are load-bearing.nm -gU <dylib> | grep -i tab(and the same forPage,IndexView) on both — confirm whether the page-style symbols differ in visibility, mangling, or presence at all.Compiler.swiftinvocation logs). Mismatched optimization levels, search paths, or-lflags will surface here.The integration-test skill already exercises both paths back-to-back, which makes capturing comparable diagnostic output cheap — restart Claude Code, run the skill against
examples/spm/, capture, then againstexamples/xcodeproj/, capture.Why this matters
xcodeprojandxcworkspaceare first-class supported build systems in PreviewsMCP, and TabView is a common SwiftUI primitive (any paged onboarding flow, dashboard, etc., would hit this). Right now anyone using PreviewsMCP against a real Xcode project to preview a TabView-containing view would see blank page bodies and have no obvious diagnostic.The integration-test skill already exercises this —
examples/xcodeproj/Sources/ToDo/ToDoView.swiftandexamples/xcworkspace/Sources/ToDo/ToDoView.swiftboth contain a TabView. The README for each example claims "first summary card (blue 'Progress') should show '1/8' with '7 remaining'", which doesn't match observed behavior. Either the bug regressed silently, or the READMEs were aspirational from the start.git log -p examples/xcodeproj/README.mdwould clarify.Out of scope
This is independent of #150 (the build-info staleness check that surfaced the bug). The two PRs touch disjoint code paths.
🤖 Filed from a Claude Code session running the
integration-testskill end-to-end to validate PR #150.