Skip to content

Instantly share code, notes, and snippets.

@fatbobman
Created September 29, 2026 06:45
Show Gist options
  • Select an option

  • Save fatbobman/94179c301e75e5a390b34888d57e9b2e to your computer and use it in GitHub Desktop.

Select an option

Save fatbobman/94179c301e75e5a390b34888d57e9b2e to your computer and use it in GitHub Desktop.
DesignProbe
import SwiftUI
// 设计锚点:在 Preview 渲染进程内收集元素坐标并写成 JSON,替代模拟器中的 XCUI 层级采集。
struct DesignAnchorEntry {
let id: String
let instance: String?
let label: String?
let bounds: Anchor<CGRect>
}
struct DesignAnchorKey: PreferenceKey {
static let defaultValue: [DesignAnchorEntry] = []
static func reduce(value: inout [DesignAnchorEntry], nextValue: () -> [DesignAnchorEntry]) {
value.append(contentsOf: nextValue())
}
}
extension View {
/// 同时设置 accessibilityIdentifier 与设计锚点;instance 用于区分列表中的同一组件。
func designAnchor(_ id: String, instance: String? = nil, label: String? = nil) -> some View {
accessibilityIdentifier(id)
// anchorPreference 会覆盖子视图的值,嵌套锚点必须用 transform 版本追加
.transformAnchorPreference(key: DesignAnchorKey.self, value: .bounds) { value, anchor in
value.append(DesignAnchorEntry(id: id, instance: instance, label: label, bounds: anchor))
}
}
/// 放在节点根视图上:把锚点坐标(window 坐标系,单位 pt)写到宿主机目录。
/// build 是编译进这次构建的源码指纹(生成的 Preview 以类型名传入),随记录写回,供 FlowCanvas 核对截图来自哪一版代码。
func designProbe(_ node: String, build: String? = nil) -> some View {
modifier(DesignProbeModifier(node: node, build: build))
}
}
private struct ProbeRecord: Codable, Equatable {
let id: String
let instance: String?
let label: String?
let frame: [Double]
}
private struct DesignProbeModifier: ViewModifier {
static let screenRecord = "__screen"
static let buildRecord = "__build"
let node: String
let build: String?
@Environment(\.colorScheme) private var colorScheme
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
func body(content: Content) -> some View {
content.overlayPreferenceValue(DesignAnchorKey.self) { entries in
GeometryReader { proxy in
let origin = proxy.frame(in: .global).origin
// 屏幕尺寸(pt):根视图加上它的安全区,供 FlowCanvas 核对 Preview 实际按哪种设备渲染
let insets = proxy.safeAreaInsets
let screen = ProbeRecord(
id: Self.screenRecord, instance: nil, label: nil,
frame: [0, 0, origin.x + proxy.size.width + insets.trailing,
origin.y + proxy.size.height + insets.bottom].map { (Double($0) * 10).rounded() / 10 }
)
let fingerprint = build.map { ProbeRecord(id: Self.buildRecord, instance: nil, label: $0, frame: [0, 0, 0, 0]) }
let records = [screen] + (fingerprint.map { [$0] } ?? []) + entries.map { entry in
let rect = proxy[entry.bounds]
let values: [CGFloat] = [rect.minX + origin.x, rect.minY + origin.y, rect.width, rect.height]
return ProbeRecord(
id: entry.id, instance: entry.instance, label: entry.label,
frame: values.map { (Double($0) * 10).rounded() / 10 }
)
}
Color.clear.task(id: records) {
write(records)
}
}
.allowsHitTesting(false)
}
}
private func write(_ records: [ProbeRecord]) {
guard let home = ProcessInfo.processInfo.environment["SIMULATOR_HOST_HOME"] else { return }
let dir = URL(filePath: home).appending(path: "Library/Caches/FlowCanvas/anchors")
let name = "\(node)__\(colorScheme == .dark ? "dark" : "light")__\(dynamicTypeSize).json"
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
try? encoder.encode(records).write(to: dir.appending(path: name))
}
}
/// 屏幕函数 `switch` 的 default 分支(原型规范 PG-10):采集键没有对应屏幕时画出可见的占位,而不是空白页。
struct UnknownCaptureKey: View {
let key: String
var body: some View {
ContentUnavailableView("没有这个采集键的屏幕", systemImage: "questionmark.square.dashed",
description: Text(key))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment