架构技术评论

用 Opus 4.8 写一个开源的 SwiftUI 视图检查器

项目是 Opus 4.8 开发的,本文也是,没怎么测试。

一个开源的运行时视图检查器——UIKit / AppKit / SwiftUI / CALayer 一棵树,全部在浏览器里看。
GitHub:https://github.com/everettjf/treescope

网站:https://everettjf.github.io/treescope

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 把客户端这一段直接换成了浏览器。换掉之后才发现,这套架构同时解决了三个问题:

  1. 零安装、跨平台
    :查看器是个网页,macOS / Linux / Windows 任意浏览器都能开。截图、提 bug、和同事对着看,都只是一个链接。
  2. 零依赖进入你的 App
    :内嵌的 HTTP/1.1 + WebSocket 服务器只用 Network.framework + CryptoKit 手写实现,不拉 Vapor / NIO,不给你的二进制塞任何第三方代码。
  3. Debug-only,无审核风险
    :服务器用 #if DEBUG 包起来,Release 构建直接排除。也正因为它从不进 Release,"即便用到私有 API 也不受 App Store 审核约束"——但实际上主路径上一行私有 API 都没用。

服务器到底跑在哪?

这点容易误解,所以强调一下:没有独立进程,也没有守护程序。Treescope.start() 是在你的 App 进程内就地起的服务器,用 NWListener 且 requiredInterfaceType = .loopback,只绑定回环 127.0.0.1:50067(被占用就向后扫描端口)。捕获引擎在主线程读取实时视图树,浏览器是唯一的外部组件。

所以"在哪台机器上跑"取决于 App 在哪跑:

App 运行环境
服务器监听处
浏览器怎么连
macOS App
Mac 的 127.0.0.1
同机直接开
iOS 模拟器
模拟器共享宿主网络栈
Mac 上 127.0.0.1 直达
真机
回环是手机自己
iproxy 50067 50067
 走 USB 转发

整体架构

Image

代码分成三个 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 分层视图

查看器默认就打开爆炸式 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/