用 Opus 4.8 写一个开源的 SwiftUI 视图检查器
项目是 Opus 4.8 开发的,本文也是,没怎么测试。
一个开源的运行时视图检查器——UIKit / AppKit / SwiftUI / CALayer 一棵树,全部在浏览器里看。
GitHub:https://github.com/everettjf/treescope网站:https://everettjf.github.io/treescope
缘起:Opus 4.8 出来之后,想认真试一下它的开发能力
Opus 4.8 发布之后,我一直想找个真实的、有点难度的项目来压一压它的开发能力——不是写个 to-do list,而是那种需要啃运行时反射、写网络服务、又要做前端交互、还得端到端测试的活儿。
正好我一直惦记一件事:开源世界里缺一个懂 SwiftUI 的视图检查器。
Lookin 是个很棒的开源工具,但它是 UIKit-only 的;而能检查 SwiftUI 的,主要是闭源的 LookInside——它的 Mac 客户端开源(GPL),但提供 SwiftUI 检查能力的运行时是闭源的签名二进制。SwiftUI 现在已经是主力,调试时却没有一个完全开源、零安装、还能看到声明树的检查器——这个空缺一直在。
于是我把这个题目丢给了 Opus 4.8(通过 Claude Code),目标定得很明确:
检查 UIKit / AppKit / SwiftUI / CALayer 一整棵视图树; SwiftUI 部分只用公开反射,不碰私有 API; 查看器是浏览器,零安装,被检查的 App 自己把它 serve 出来; 接入零依赖、只在 Debug 生效,不污染 Release。
这里有一个刻意和 Lookin 区别开的架构决定值得单独说:Lookin / LookInside 都是「原生 Mac 客户端 App + 嵌进你 App 的 server」的两段式——你得另装一个客户端。我干脆把那个原生客户端整个换成了浏览器:被检查的 App 自己把查看器 serve 出来,你打开一个 URL 就行。这一是为了和它们做出区分,二是顺带白嫖了 Web 的红利——跨平台、零安装、天然好分享。后面会看到,这个选择几乎决定了整个项目的形态。
这就是 Treescope。下面先讲它是什么、怎么工作,最后聊聊"和 Opus 4.8 一起把它写出来"是种什么体验。
Treescope 是什么
把 Treescope 加进你的 App,它会在进程内起一个极小的本地服务器;然后你在任意浏览器打开 http://127.0.0.1:50067,就能看到这个正在运行的 App 的完整视图层级:
左边是统一的层级树:UIKit/AppKit 视图、SwiftUI 节点、CALayer 全在一棵树里,按框架配色,支持搜索、过滤、隐藏系统视图、键盘导航; 中间是画布:线框 + 渲染快照,还有一个爆炸式 3D 分层视图,可以拖拽旋转、缩放、平移; 右边是属性面板:分类展示属性,部分属性还能实时编辑(改 alpha、圆角、颜色、文字……App 里立刻生效),并能在设备上高亮选中的视图。
没有第二个 App 要装,不用连线,分享调试现场就是发一个 URL。
为什么是"浏览器查看器 + 进程内服务器"
如前所述,这是为了和 Lookin 不同而刻意做的选择:Lookin / LookInside 是「原生 Mac 客户端 + App 内 server」,Treescope 把客户端这一段直接换成了浏览器。换掉之后才发现,这套架构同时解决了三个问题:
- 零安装、跨平台
:查看器是个网页,macOS / Linux / Windows 任意浏览器都能开。截图、提 bug、和同事对着看,都只是一个链接。 - 零依赖进入你的 App
:内嵌的 HTTP/1.1 + WebSocket 服务器只用 Network.framework+CryptoKit手写实现,不拉 Vapor / NIO,不给你的二进制塞任何第三方代码。 - Debug-only,无审核风险
:服务器用 #if DEBUG包起来,Release 构建直接排除。也正因为它从不进 Release,"即便用到私有 API 也不受 App Store 审核约束"——但实际上主路径上一行私有 API 都没用。
服务器到底跑在哪?
这点容易误解,所以强调一下:没有独立进程,也没有守护程序。Treescope.start() 是在你的 App 进程内就地起的服务器,用 NWListener 且 requiredInterfaceType = .loopback,只绑定回环 127.0.0.1:50067(被占用就向后扫描端口)。捕获引擎在主线程读取实时视图树,浏览器是唯一的外部组件。
所以"在哪台机器上跑"取决于 App 在哪跑:
| macOS App | ||
| iOS 模拟器 | ||
| 真机 | iproxy 50067 50067 |
整体架构
代码分成三个 Swift target + 一个前端:
TreescopeProtocol:纯 Foundation 的线缆数据模型,用一个 t字段做可辨识联合的 JSON 协议,TypeScript 端一一对应。消息有handshake / fetchHierarchy / fetchSnapshot / setAttribute / highlight / ping以及对应的响应和hierarchyChanged等事件。TreescopeServer:注入 App 的 Debug-only 运行时——捕获引擎 + HTTP/WS 服务器 + 内嵌的查看器(前端构建成单文件 HTML,用 Bundle.module在运行时读出来发给浏览器)。这是别人要依赖的产品。TreescopeDemo和 Examples/:示例 App,用于端到端测试。Web/:React + TypeScript + Tailwind + shadcn/ui 的浏览器查看器。
规模大概是 ~3,400 行 Swift(24 个文件)+ ~1,900 行 TS/TSX(25 个文件),外加 53 个 Swift 测试。
技术亮点:用 Mirror 把 SwiftUI 反射出来
最难、也最有意思的部分是 SwiftUI 检查。SwiftUI 的 View 是值类型、强类型擦除,运行时拿不到现成的"视图树"。Treescope 的做法是只用公开反射:
打开 any View这个存在类型,用Mirror检查它的具体类型;- 组合器
( ModifiedContent/TupleView/Group/AnyView/_ConditionalContent/Optional)按结构拆开; - 你自己的视图
(有真正的 body)会被递归进body,从而还原出声明树——VStack、带内容的Text、各种 modifier、@State; - 框架原生视图
( SwiftUI*模块的)则绝不调用body(它可能是Never或依赖活的渲染上下文),而是扫描存储属性来发现子视图,把值得展示的字段变成属性。
还有一个细节是活值(live values):视图一旦被 host,SwiftUI 会把 @State/@StateObject 装到活的 AttributeGraph 上。Treescope 对引用类型的可观察状态是真·实时读取的——一个 @ObservedObject 模型按引用共享,它当前的 @Published 字段会被标成 (live),模型一变下次捕获就能看到。
配合 CALayer 的遍历(对 SwiftUI host 强制打开),画布上还能拿到真实的已解析渲染几何。整条 SwiftUI 路径没有 _viewDebugData / AttributeGraph 私有 API。
浏览器查看器:一块"像 Figma 一样"的画布
查看器默认就打开爆炸式 3D 视图,能直观看到层级的纵深。画布的交互我专门按现代设计工具的最佳实践打磨过——这也是开发过程里反复迭代的地方:
- 双指滑动 / 滚轮 → 平移
(任意方向,2D 和 3D 都一样); - 触控板捏合 / ⌘-滚轮 → 缩放,且锚定在光标位置
- 拖拽
→ 2D 平移、3D 旋转;空白处、节点上都能拖; - 轻点 → 选中
光标下的节点; 右下角有悬浮的缩放 / 角度 / Reset 控件。
这套模型来自 Figma / tldraw / Excalidraw 的共识:浏览器会把触控板捏合伪装成带 ctrlKey 的 wheel 事件,所以用 ctrlKey/metaKey 区分"缩放 vs 平移",再对 deltaY 做钳制,让鼠标滚轮的大跳变和触控板捏合的小增量手感一致。
下面这段录屏覆盖了缩放到光标、拖拽平移、3D 爆炸 + 旋转:
怎么用
接入是标准的 SwiftPM,加 GitHub URL 即可:
// Package.swift
dependencies: [
.package(url: "https://github.com/everettjf/treescope.git", from: "0.1.0"),
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "TreescopeServer", package: "treescope"),
]),
]
Xcode 里就是 File ▸ Add Package Dependencies… 粘贴那个 URL,加上 TreescopeServer。然后在 App 启动早期、用 Debug 包起来启动一次:
import TreescopeServer
#if DEBUG
Treescope.start() // 默认 serve http://127.0.0.1:50067
#endif
跑起来,浏览器打开 http://127.0.0.1:50067 就行。仓库里还有 iOS 模拟器和 macOS 两个可直接运行的接入示例(Examples/),各自带一个 ./run.sh(生成→编译→启动→校验)和无头 WebSocket 验证脚本。
和 Opus 4.8 一起开发,是种什么体验
这才是这篇博客真正想记录的部分。整个项目几乎是我"出题 + 把关",Opus 4.8 在 Claude Code 里端到端实现的。几个让我印象深刻的点:
1. 它会真的去"跑",而不是只写代码。
做画布交互时,它没有写完就算完,而是用 puppeteer-core 驱动我本机的 Chrome,真实派发滚轮 / 拖拽 / 点击事件,再读 DOM 的 transform 来断言行为对不对。验证"缩放锚定光标"时,它算出光标下的内容坐标在缩放前后的偏差是 0.09px——这种级别的自证,是我没料到的。一轮交互测试 16 项全过才算数。
2. 它能在测试里发现自己写的真 bug。
有一次它把"滚轮缩放"重构进了一个原生非被动监听器,结果端到端测试里滚轮完全不动。它自己定位到根因:监听器的 useEffect 依赖是 [],只在挂载时跑一次,而那一刻画布还没数据、提前 return 了、ref 是 null,所以监听器从没绑上。然后它修了依赖、加了"点击 vs 拖拽"的 4px 阈值消歧,让"任意处拖拽平移 + 轻点选中"都成立。
3. 决策是有依据的,不是拍脑袋。
我说"中间滚动体验不太好",它没有瞎调参数,而是去搜了 Figma / tldraw / Excalidraw 的做法,确认了"ctrlKey 区分捏合 + deltaY 钳制"这个业界共识,再按它实现,并给出了引用来源。
4. 工程化的事它也顺手做了。
它写了个 deploy.sh 一键发布脚本(从最新 tag 自动 bump 版本、重建并内嵌前端、跑测试、改 CHANGELOG、打 tag、push、建 GitHub Release),还建了个临时的空 package 去验证"外部用户能从 GitHub URL 解析 + 编译"整条链路。53 个 Swift 测试一路绿着。
几个数字:~3,400 行 Swift + ~1,900 行 TypeScript,手写的 HTTP/1.1 + WebSocket(含 RFC 6455 的帧编解码、Sec-WebSocket-Accept 计算),SwiftUI 的 Mirror 反射,React 查看器——这些跨度很大的东西,能在一个会话里被串起来并且端到端跑通,是这次实验最直接的结论。
当然它不是没有边界:捕获引擎里坐标系翻转(AppKit)、CALayer 几何、Mirror 对纯 SwiftUI 生命周期窗口根的盲区,这些有约束的地方仍然需要人来定方向、判断对错。但"把一个真实项目从 0 推到可发布"这件事,Opus 4.8 给我的体感是——它确实能当一个靠谱的实现者,而我更多是在做产品和质量上的把关。
小结
Treescope 现在是 v0.1.0,MIT 协议,单仓库可直接 swift build,通过 SwiftPM 接入即用。它补上了开源世界里"懂 SwiftUI 的视图检查器"这个空缺,全部能力都开放。
而对我来说,它更是一次"Opus 4.8 能不能扛真实项目"的实测——答案偏正面。如果你也在调 SwiftUI、或者想看看 AI 协作开发能走到哪一步,欢迎去仓库逛逛、点个 star:
GitHub:https://github.com/everettjf/treescope 网站 & 教程:https://everettjf.github.io/treescope/