-
-
Save fatbobman/94179c301e75e5a390b34888d57e9b2e to your computer and use it in GitHub Desktop.
DesignProbe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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