Skip to content

Commit c1797c2

Browse files
authored
Add SwiftUI view tree support (#6)
* Add SwiftUI view tree support * Add SwiftUI preview runner docs and npm script
1 parent 75aec3c commit c1797c2

19 files changed

Lines changed: 2126 additions & 21 deletions

File tree

client/src/api/types.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,14 @@ export interface AccessibilityNode {
8181
role?: string | null;
8282
role_description?: string | null;
8383
scroll?: Record<string, unknown> | null;
84-
source?: "native-ax" | "in-app-inspector" | "nativescript" | string | null;
84+
source?:
85+
| "native-ax"
86+
| "in-app-inspector"
87+
| "nativescript"
88+
| "react-native"
89+
| "swiftui"
90+
| string
91+
| null;
8592
sourceColumn?: number | null;
8693
sourceFile?: string | null;
8794
sourceLine?: number | null;
@@ -102,7 +109,8 @@ export type AccessibilitySource =
102109
| "native-ax"
103110
| "in-app-inspector"
104111
| "nativescript"
105-
| "react-native";
112+
| "react-native"
113+
| "swiftui";
106114
export type AccessibilitySourcePreference = AccessibilitySource | "auto";
107115

108116
export interface AccessibilityTreeResponse {

client/src/app/AppShell.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import {
7070
const ACCESSIBILITY_REFRESH_MS = 1500;
7171
const REACT_NATIVE_ACCESSIBILITY_REFRESH_MS = 500;
7272
const DEFAULT_ACCESSIBILITY_MAX_DEPTH = 10;
73-
const REACT_NATIVE_ACCESSIBILITY_MAX_DEPTH = 60;
73+
const LOGICAL_INSPECTOR_MAX_DEPTH = 80;
7474

7575
clearLegacyVolatileUiState();
7676

@@ -420,7 +420,7 @@ export function AppShell() {
420420
maxDepth:
421421
accessibilityPreferredSource === "native-ax"
422422
? DEFAULT_ACCESSIBILITY_MAX_DEPTH
423-
: REACT_NATIVE_ACCESSIBILITY_MAX_DEPTH,
423+
: LOGICAL_INSPECTOR_MAX_DEPTH,
424424
},
425425
);
426426
if (accessibilityRequestIdRef.current !== requestId) {
@@ -443,6 +443,12 @@ export function AppShell() {
443443
accessibilityPreferredSource !== "nativescript"
444444
) {
445445
setAccessibilityPreferredSource("nativescript");
446+
} else if (
447+
snapshot.source === "native-ax" &&
448+
availableSources.includes("swiftui") &&
449+
accessibilityPreferredSource !== "swiftui"
450+
) {
451+
setAccessibilityPreferredSource("swiftui");
446452
}
447453
if (
448454
accessibilityPreferredSource !== "auto" &&

client/src/app/uiState.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ describe("uiState", () => {
1414
sanitizeAccessibilitySources([
1515
"native-ax",
1616
"unknown",
17+
"swiftui",
1718
"nativescript",
1819
"native-ax",
1920
"in-app-inspector",
2021
]),
21-
).toEqual(["nativescript", "in-app-inspector", "native-ax"]);
22+
).toEqual(["nativescript", "swiftui", "in-app-inspector", "native-ax"]);
2223
});
2324

2425
it("sanitizes persisted viewport state and falls back to defaults", () => {

client/src/app/uiState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const ACCESSIBILITY_SOURCE_STORAGE_KEY = "xcw-hierarchy-source";
2828
const ACCESSIBILITY_SOURCE_ORDER: AccessibilitySource[] = [
2929
"nativescript",
3030
"react-native",
31+
"swiftui",
3132
"in-app-inspector",
3233
"native-ax",
3334
];
@@ -115,6 +116,7 @@ export function isAccessibilitySource(
115116
return (
116117
value === "nativescript" ||
117118
value === "react-native" ||
119+
value === "swiftui" ||
118120
value === "in-app-inspector" ||
119121
value === "native-ax"
120122
);

client/src/features/accessibility/AccessibilityInspector.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,7 @@ function errorMessage(error: unknown): string {
726726
const HIERARCHY_SOURCE_ORDER: AccessibilitySource[] = [
727727
"nativescript",
728728
"react-native",
729+
"swiftui",
729730
"in-app-inspector",
730731
"native-ax",
731732
];
@@ -754,6 +755,9 @@ function sourceLabel(source: AccessibilitySource): string {
754755
if (source === "react-native") {
755756
return "React Native";
756757
}
758+
if (source === "swiftui") {
759+
return "SwiftUI";
760+
}
757761
return source === "in-app-inspector" ? "UIKit" : "Native AX";
758762
}
759763

@@ -815,8 +819,12 @@ function swiftUIDescription(value: Record<string, unknown> | null | undefined) {
815819
const flags = [
816820
value.isHost === true ? "host" : "",
817821
value.isProbe === true ? "probe" : "",
822+
value.isViewTreeNode === true ? "view tree" : "",
818823
].filter(Boolean);
819-
return [tag, tagId, flags.join(", ")].filter(Boolean).join(" / ");
824+
const modifiers = Array.isArray(value.modifiers)
825+
? value.modifiers.filter((item) => typeof item === "string").join(", ")
826+
: "";
827+
return [tag, tagId, flags.join(", "), modifiers].filter(Boolean).join(" / ");
820828
}
821829

822830
function frameText(frame: {

client/src/styles/components.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,12 @@
426426
color: color-mix(in srgb, #61dafb 78%, var(--text));
427427
}
428428

429+
.hierarchy-source-pill.source-swiftui {
430+
border-color: color-mix(in srgb, #ff6b9d 50%, var(--border));
431+
background: color-mix(in srgb, #ff6b9d 14%, transparent);
432+
color: color-mix(in srgb, #ff6b9d 82%, var(--text));
433+
}
434+
429435
.hierarchy-source-pill.source-native-ax {
430436
border-color: color-mix(in srgb, #d7ba7d 55%, var(--border));
431437
background: color-mix(in srgb, #d7ba7d 13%, transparent);

docs/api/inspector-protocol.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,20 @@ Evaluates a small UIKit script against a view. Used by the browser inspector to
248248

249249
## SwiftUI
250250

251-
SwiftUI's value tree is not publicly enumerable at runtime. The agent therefore exposes SwiftUI in two ways:
251+
For SwiftUI apps you control, attach the root publisher to the top of your scene:
252+
253+
```swift
254+
WindowGroup {
255+
ContentView()
256+
.simDeckPublishSwiftUIViewTree("ContentView", id: "app.root")
257+
}
258+
```
259+
260+
The agent reflects the current SwiftUI value/body tree and publishes it as the `swiftui` hierarchy source. `View.getHierarchy` returns that tree by default; pass `"source": "uikit"` to inspect the backing hosting views instead.
261+
262+
This is a debug aid built on Swift reflection. It can show the declared view/body structure, including custom subviews, containers, labels, modifier names, active conditional branches, and `ForEach` rows whose data and content builder are available through SwiftUI's public API. Private/custom containers may still be opaque when they do not expose a child view value or content builder.
263+
264+
The agent also exposes SwiftUI in the raw UIKit tree:
252265

253266
1. **Automatic detection.** UIKit bridge or hosting views whose runtime classes contain `SwiftUI` or `UIHosting` are reported with `swiftUI.isHost` or `swiftUI.isProbe` markers.
254267
2. **Source-level tags.** Apps can tag SwiftUI views with `View.simDeckInspectorTag(_:id:metadata:)` from the Swift agent. Tagged views appear as lightweight probe `UIView`s with `swiftUI.isProbe = true`.

docs/api/rest.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,13 +307,14 @@ Returns the rendered bezel as a PNG. Cache headers are set to `no-cache, no-stor
307307

308308
### `GET /api/simulators/{udid}/accessibility-tree`
309309

310-
Returns the current accessibility tree. The server merges three sources: NativeScript, Swift in-app agent (UIKit), and accessibility tree. Query parameters:
310+
Returns the current accessibility tree. The server merges framework inspectors, the Swift in-app agent, and the native accessibility tree. Query parameters:
311311

312312
| `source` | Behaviour |
313313
| ---------------------------- | ------------------------------------------------------------------------------------------------------ |
314314
| `auto` _(default)_ / unset | Use the most accurate source available, falling back to AX. |
315315
| `nativescript` / `ns` | Force the NativeScript logical tree if a NativeScript inspector is connected for the foreground app. |
316316
| `react-native` / `rn` | Force the React Native component tree if a React Native inspector is connected for the foreground app. |
317+
| `swiftui` / `swift-ui` | Force the published SwiftUI logical tree if the Swift agent root publisher is installed in the app. |
317318
| `uikit` / `in-app-inspector` | Force the raw UIKit hierarchy from the in-app inspector agent (NativeScript or Swift). |
318319
| `native-ax` / `ax` | Always use the native accessibility snapshot. |
319320

@@ -327,8 +328,8 @@ The response always includes:
327328
```json
328329
{
329330
"roots": [...],
330-
"source": "nativescript|react-native|in-app-inspector|native-ax",
331-
"availableSources": ["nativescript", "react-native", "in-app-inspector", "native-ax"],
331+
"source": "nativescript|react-native|swiftui|in-app-inspector|native-ax",
332+
"availableSources": ["nativescript", "react-native", "swiftui", "in-app-inspector", "native-ax"],
332333
"fallbackReason": "...",
333334
"inspector": { ... }
334335
}

docs/inspector/accessibility.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ It reports anything the app publishes through the accessibility tree:
1212

1313
It does **not** see:
1414

15-
- SwiftUI value-tree internals.
15+
- SwiftUI value-tree internals unless the app links the Swift agent and attaches the SwiftUI root publisher.
1616
- NativeScript logical tree nodes.
1717
- UIView properties that aren't part of the accessibility surface.
1818

19-
For those, you need to link the [Swift in-app agent](/inspector/swift) or use the [NativeScript runtime inspector](/inspector/nativescript).
19+
For those, you need to link the [Swift in-app agent](/inspector/swift), attach the SwiftUI root publisher, or use the [NativeScript runtime inspector](/inspector/nativescript).
2020

2121
## When AX is the right call
2222

docs/inspector/index.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The HTTP API picks the most specific source available, falls back to the next on
2020
| `auto` _(default)_ / unset | Use the most accurate source available, falling back to AX. |
2121
| `nativescript` / `ns` | Force the NativeScript logical tree if a NativeScript inspector is connected for the foreground app. |
2222
| `react-native` / `rn` | Force the React Native component tree if a React Native inspector is connected for the foreground app. |
23+
| `swiftui` / `swift-ui` | Force the published SwiftUI logical tree if the Swift agent root publisher is installed in the app. |
2324
| `uikit` / `in-app-inspector` | Force the raw UIKit hierarchy from any in-app inspector (NativeScript or Swift agent). |
2425
| `native-ax` / `ax` | Always use the native accessibility snapshot. |
2526

@@ -33,10 +34,11 @@ Every accessibility tree response includes:
3334

3435
```json
3536
{
36-
"source": "nativescript|react-native|in-app-inspector|native-ax",
37+
"source": "nativescript|react-native|swiftui|in-app-inspector|native-ax",
3738
"availableSources": [
3839
"nativescript",
3940
"react-native",
41+
"swiftui",
4042
"in-app-inspector",
4143
"native-ax"
4244
],
@@ -49,7 +51,7 @@ Every accessibility tree response includes:
4951

5052
## Choosing the right inspector
5153

52-
- **You own the iOS app and write Swift / Objective-C.** Link the [Swift in-app agent](/inspector/swift). It exposes the most semantic data — UIView properties, SwiftUI probes, custom actions — and lets the browser client edit values in place.
54+
- **You own the iOS app and write Swift / Objective-C.** Link the [Swift in-app agent](/inspector/swift). It exposes the most semantic data — UIView properties, SwiftUI view trees/probes, custom actions — and lets the browser client edit values in place.
5355
- **You ship a NativeScript app.** Use the [NativeScript runtime inspector](/inspector/nativescript). It connects outbound to the SimDeck server and publishes both the NativeScript logical tree and the underlying UIKit hierarchy.
5456
- **You ship a React Native app.** Use the [React Native runtime inspector](/inspector/react-native). It connects outbound to the SimDeck server and publishes the React component tree with dev-mode source locations.
5557
- **You can't link anything into the app.** Stick with [AX snapshot](/inspector/accessibility). It only sees what the iOS accessibility stack exposes, but it works for every app.

0 commit comments

Comments
 (0)