☰
Peekaboo 现代 Swift 与 SwiftUI 代码规范指南:从状态管理、并发边界到验证纪律
2026/10/13 6:10:40 网站建设 项目流程
  • GUI 自动化
  • MCP 服务
  • AI 应用

【免费下载链接】Peekaboo

Peekaboo is a macOS CLI & optional MCP server that enables AI agents to capture screenshots of applications, or the entire system, with optional visual question answering through local or remote AI models.

项目地址:https://gitcode.com/gh_mirrors/peekaboo6/Peekaboo
点击查看免费下载

本篇指南以 Peekaboo 仓库的 docs/modern-swift.md 为骨架,结合仓库内真实的 Swift 源码、SwiftLint/SwiftFormat 配置与测试代码展开。读者将掌握 Peekaboo 在 Swift 6.2 严格并发模式下的编码约定:视图状态如何放置、可观察模型如何组织、阻塞调用如何与异步边界隔离、以及在重构时如何守住"已发布 API 与线格式"的兼容底线。适合任何要为 Peekaboo 提交 Swift/SwiftUI 改动(无论是 macOS 应用、CLI 还是 Core 各模块)的开发者阅读。

一、总则:Swift 6.2 约定、显式self与工具链基线

Peekaboo 的 Swift 代码遵循Swift 6.2 约定,并强制在成员引用处使用显式self。文档明确要求:提交改动前必须运行仓库自带的 SwiftFormat 与 SwiftLint,同时不得为了某个重构能编译而擅自抬高部署目标——这条规则在 docs/platform-support.md 中有完整的基线矩阵。

1.1 工具链与部署目标基线

从仓库的包清单可以确认,所有 SwiftPM 包一律声明swift-tools-version: 6.2,例如:

  • Apps/CLI/Package.swift
  • Apps/Mac/Package.swift
  • Core/PeekabooAutomationKit/Package.swift
  • Core/PeekabooCore/Package.swift
  • Core/PeekabooFoundation/Package.swift

platform-support.md 给出的基线为:

表面最低要求说明
已发布的 CLI 与 MCP 包macOS 15.0+Apps/CLI/Package.swift声明.macOS(.v15),npm 包包装同一 CLI 二进制
macOS 应用macOS 15.0+Xcode 工程设置MACOSX_DEPLOYMENT_TARGET = 15.0
公共 SwiftPM 库macOS 14.0+(包元数据)根Package.swift导出PeekabooFoundation、PeekabooProtocols、PeekabooAutomationKit、PeekabooBridge
CLI 源码构建macOS 15.0+、Swift 6.2+ 工具链维护者与 CI 使用 Xcode 26.x 或更新版本

因此,任何重构都应"就着现有地板"完成,而不是把 deployment target 抬高一档来换取编译器放行。

1.2 格式化与静态检查配置

仓库根目录的 .swiftformat 与 .swiftlint.yml 定义了唯一权威配置:

  • SwiftFormat 使用--swiftversion 6.2、--self insert(插入self,适配 Swift 6 并发要求的成员引用)、--maxwidth 120、4 空格缩进、before-first的参数换行风格、MARK: - %t + %p的类型/扩展标记;
  • SwiftLint 的included覆盖Apps与Core,排除.build、DerivedData、**/Generated、**/Resources、Package.swift等;根配置通过parent_config: .swiftlint-rules.yml继承规则集(Apps/CLI下另有独立的 .swiftlint.yml 用于 CLI 目标);
  • 少数"必须保持原样"的文件被显式排除在格式化之外,例如Core/PeekabooCore/Sources/PeekabooCore/Extensions/NSArray+Extensions.swift。

这条链路的用意很明确:格式与 lint 不是可选项,而是提交前必须通过的入口检查。

二、状态与视图:@State、@Binding、@Observable与@Bindable的摆放规则

文档给出的状态管理纪律可以概括为一句话:让状态靠近它的所有者。视图局部值放@State,可写值经@Binding下传,共享依赖走已有 environment 或初始化器注入。

2.1 视图局部状态:@State

Apps/Mac/Peekaboo/Features/Settings/SettingsWindow.swift 是教科书式的例子:

@State private var selectedTab: PeekabooSettingsTab = .general @State private var monitoringPermissions = false

同一文件中还出现了典型的"一次性检测状态"(@State private var hasAttemptedOllamaDetection = false),说明@State只承载视图生命周期内、无共享需求的值。

2.2 可写值下传:@Binding

GeneralSettingsView把开关行拆成独立组件时,通过@Binding传递写权限:

@Binding var showInDock: Bool @Binding var isOn: Bool

这正对应文档"pass writable values through@Binding"的约定——子视图只拿到"读写通道",不拥有数据本体。

2.3 可观察模型:@Observable+@MainActor

文档允许新代码使用 Observation 框架,同时要求:UI 持有的模型隔离到@MainActor,由拥有它的视图通过@State持有。仓库的落地非常一致:

  • Apps/Mac/Peekaboo/Core/Settings.swift 注释明确写着 "This class uses the modern @Observable pattern for SwiftUI integration";
  • Apps/Mac/Peekaboo/Core/ConversationSession.swift 是标准模板:
@Observable @MainActor final class SessionStore { var sessions: [ConversationSession] = [] var currentSession: ConversationSession? ... }
  • 同一目录下的PeekabooAgent.swift、Permissions.swift、ProviderCredentialCoordinator.swift,以及 Features 中的AutomationTargetTracker.swift都采用相同的@Observable模式。

2.4 子视图取绑定:@Bindable

当子视图需要把可观察模型的属性变成绑定(例如切换开关)时,使用@Bindable。SettingsWindow.swift中典型的局部写法:

@Environment(PeekabooSettings.self) private var settings var body: some View { @Bindable var settings = self.settings Form { SettingsToggleRow(..., isOn: $settings.launchAtLogin) ... } }

这里$settings.launchAtLogin正是由@Bindable提供的属性绑定。整套组合——@State持有、@Bindable取绑定、@Binding下传——就是文档所述"Keep state near its owner"在 Peekaboo 中的具体形态。

2.5 何时抽模型、何时抽视图

文档还有两条微妙的边界:

  • 抽模型:当某段状态或行为需要被多处共享时,从视图里提炼出来;
  • 抽视图:当某个子树有了清晰职责时再抽视图;
  • 小视图不必配套模型:不要为了"对称"给每个小组件硬造一个 model 类型。

此外,已发布的ObservableObject契约要原样保留,除非这次改动明确迁移了它的调用方。这与下文"兼容性红线"一脉相承。

三、渲染与异步工作:.task、.task(id:)与TimelineView

3.1 渲染代码不得夹带 I/O

文档要求"rendering code free of I/O and repeated expensive work"。这是 SwiftUI 性能的基本盘:body只负责声明视图结构,真正的异步加载放.task。

3.2.task与.task(id:)的选用

  • .task:绑定视图生命周期的异步工作,视图消失即取消;
  • .task(id:):当某个具体输入变化时应重启任务。

仓库中的真实用法在 SettingsWindow.swift:

.task(id: self.settings.ollamaBaseURL) { await self.refreshOllamaModels() }

当用户修改 Ollama 服务地址时,模型列表的探测任务会随id变化自动重启——这正是"用id表达重启语义"的典型场景。对应的refreshOllamaModels()被标注为@MainActor,与@Observable模型的隔离保持一致。

3.3 时间驱动的呈现:TimelineView与事件流

文档给出了取舍原则:新代码的时间驱动呈现优先用TimelineView(如果其调度语义契合功能);当事件流本身就是真相源时,保留事件流。也就是说,不要为了"看起来新"而用TimelineView去重实现一个本就基于流订阅的界面。

3.4 取消与失败要分开处理

"Handle cancellation separately from a user-visible failure"——取消是调用方主动结束等待,不应被包装成一次面向用户的错误提示。这与下一节并发边界的原则互为表里。

四、并发与服务边界:尊重 actor 隔离,不为编译而作弊

4.1 禁止用nonisolated(unsafe)/@unchecked Sendable堵编译器

文档的态度非常明确:尊重 actor 隔离,而不是用nonisolated(unsafe)或@unchecked Sendable来"静音"编译器。当类型确实需要用锁或队列保护共享状态时:

  1. 所有对该状态的访问必须留在同步边界内;
  2. 必须在注释里说明为什么这个 unchecked 一致性是健全的(sound)。

仓库中那些合法的@unchecked Sendable都有明确理由。例如 ElementDetectionTimeoutRunner.swift 中的AXObservationWorkerPool:它用NSLock保护lanes字典,所有读写都经过self.lock.withLock { ... }(见其enqueue、complete、evictIdleLanesIfNeeded方法),因此@unchecked Sendable是有同步机制兜底的。同理,ScreenCaptureKitOwnerLease.swift 用NSLock保护nonisolated(unsafe) static var的进程描述符注册表,每个可变静态状态旁边都配一把专用锁。

4.2 actor 与跨边界值:优先Sendable结构体

跨 actor 与传输边界传递的数据应尽量是类型化的、Sendable的值。仓库中大量 actor 与异步服务之间交换的是显式Sendable结构体,例如:

  • ActionVerifier.swift 的ActionDescriptor/VerificationResult;
  • AgentCompatibilityTypes.swift 的AgentExecutionResult;
  • AgentToolMCPBridge.swift 的actor AgentToolMCPImageStore。

actor EventHandler(PeekabooAgentService+Execution.swift)则展示了"让状态归 actor、跨 actor 只传值"的经典布局。

4.3 关键洞察:async不会让阻塞调用变非阻塞

文档点出了 Peekaboo 并发模型中最反直觉的一条:把阻塞性 Accessibility 或 socket 调用包进async函数,并不会让它变成非阻塞。Accessibility RPC 与 socket 读写在原生层面是不协作取消的,取消只能结束调用方等待,不能中断后台原生工作。

因此 Peekaboo 在这些边界使用有界的原生 worker 与传输队列,并完整保留"入场(admission)、截止时间(deadline)、清理(cleanup)"规则。最完整的实例是 ElementDetectionTimeoutRunner.swift:

  • run(seconds:operation:)用withCheckedThrowingContinuation装配一个"截止时间状态机",主操作放Task { @MainActor ... },而超时计时器刻意放在Task.detached——注释明确写着 "The timer must not inherit MainActor",防止同步的 MainActor 操作靠饿死 deadline 任务来"假成功";
  • runDetached(...)把非变异 AX 工作投递到per-process 串行 lane(AXObservationWorkerPool按pid + processStartIdentity建 lane,每个 lane 一个DispatchQueue),并支持maximumPendingOperationCount限流与空闲 lane 驱逐(上限 64);
  • 每个 AX 调用有软/硬两级超时(DetachedAXWindowEnumerationTiming:硬超时减去 0.01~0.25s 的完成宽限得到软超时),超时后立即恢复调用方,绝不 join 仍在飞行中的 AX RPC。

这就是"async函数不会让阻塞调用非阻塞"的正解:在原生边界保留有界 worker 和专用队列,把"等待"与"原生工作"彻底分离。

4.4 结构化并发与 detached task 的所有权

文档规定:子工作的生命周期属于调用方时,优先用结构化并发;Task.detached必须有明确的生命周期与取消所有者。上述超时任务是有意脱离调用方生命周期(它必须独立于 MainActor 运行)的少数例外,且每个 detached task 都在onCancel中通过state.cancel()收尾。

4.5 模块职责边界

  • 生产自动化逻辑放在AutomationKit;
  • Agent / MCP 适配放在AgentRuntime;
  • 共享可视组件放在UICore 或 Visualizer。

从目录结构看,Core/PeekabooAutomationKit(自动化原语)、Core/PeekabooCore/Sources/PeekabooAgentRuntime(Agent 运行时)、Core/PeekabooUICore/Core/PeekabooVisualizer(UI 与可视化)正是这条分层的物理体现。

五、验证纪律:在所有权边界测试,不靠 sleep

5.1 测试应落在行为的"所属边界"

文档的验证原则是"Exercise behavior at its owning boundary":能注入时钟、服务、临时存储的接缝处就注入;用可观察的完成等待结果,而不是sleep一段估算时长。

仓库测试大量使用时钟注入来避免睡眠:Core/PeekabooAutomationKit中的ScreenCaptureKitCaptureGate、ScreenCaptureKitWindowPlanCache等实现都以ContinuousClock.Instant作为截止时间输入(见 ScreenCaptureKitCaptureGate.swift 与 ScreenCaptureKitWindowPlanCache.swift),测试据此可以精确控制"现在时刻"来验证过期与超时路径。

5.2 已发布 API 与线格式的测试必须保留

重构时,已发布的 API 与已确定线格式(wire format)的测试不得删除。这条规则在 docs/dev/lint-compatibility.md 中有严格登记:

  • DesktopTargetEvidenceAdapter.evidence(processIdentifier:...)(DesktopTargetEvidenceAdapter.swift)因六个独立输入是 v4.2.2 发布契约,持有唯一一个function_parameter_count豁免,且由版本选择的 SwiftPM 消费者 fixture(scripts/test-swiftpm-consumer.sh)做编译验证;
  • 三个用@available(*, deprecated)上下文故意调用 v4.2.2 已废弃公共 API 的测试声明(PeekabooBridgeAppleScriptCompatibilityTests、InMemorySnapshotManagerTests的两个 legacy initializer 用例)被点名保留,豁免范围只限这三处声明本身,不授权整文件注释、编译警告抑制或兼容垫片。

5.3 提交前的验证流程

文档给出的最终检查清单是:

  1. 构建受影响的包并运行相关测试;
  2. UI 行为改动还要运行构建出的应用、对照受控 fixture 验证可见结果——SwiftUI previews只辅助布局,不能证明生命周期、权限或自动化正确性;
  3. 遵循 docs/building.md 与docs/testing/下的相应契约完成检查。

六、总结:一份可执行的"改动前 Checklist"

把整份规范浓缩成提交前的自查清单:

  • 工具链为 Swift 6.2,改动不抬高任何包的 deployment target(对照 docs/platform-support.md);
  • 成员引用显式写self,代码通过仓库 .swiftformat 与 .swiftlint.yml 检查;
  • 视图局部值用@State,可写值用@Binding下传,共享依赖走 environment/初始化器注入;
  • 新可观察模型用@Observable+@MainActor,由@State持有;子视图绑定用@Bindable;既有ObservableObject公共契约未动;
  • 渲染代码无 I/O;异步工作挂.task/.task(id:),取消与用户可见失败分开处理;
  • 没有用nonisolated(unsafe)/@unchecked Sendable堵编译器;锁保护的共享状态全部访问都在同步边界内,unchecked 一致性有注释论证;
  • 阻塞的 AX/socket 调用走有界原生 worker 与传输队列,worker 的入场/截止时间/清理规则原样保留;
  • 子工作优先结构化并发;detached task 有明确生命周期与取消所有者;
  • 测试落在所有权边界,用注入的时钟/服务/临时存储,用可观察完成代替 sleep;
  • 已发布 API 与线格式测试完整保留,lint 豁免符合 docs/dev/lint-compatibility.md 的登记范围;
  • 构建受影响包、跑相关测试;UI 改动再用受控 fixture 跑真实应用验证。

这份指南既是新人快速进入 Peekaboo 的编码入口,也是老改动合入前唯一的"合规裁判"。遵循它,你的改动才能同时通过编译器、SwiftLint、测试与发布契约四道关卡。

  • GUI 自动化
  • MCP 服务
  • AI 应用

【免费下载链接】Peekaboo

Peekaboo is a macOS CLI & optional MCP server that enables AI agents to capture screenshots of applications, or the entire system, with optional visual question answering through local or remote AI models.

项目地址:https://gitcode.com/gh_mirrors/peekaboo6/Peekaboo
点击查看免费下载
上一篇:GetQzonehistory:免费完整导出 QQ 空间历史说说到本地 Excel 的备份工具
下一篇:如何安全使用Facebook-scraper:避免封禁的终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询