ECC 仓库中的 Swift 设计模式指南:协议导向、值类型、Actor 并发与依赖注入
【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC
本文围绕开源仓库 GitHub_Trending/ev/ECC 中面向 Swift 开发的 steering 规则文档 .kiro/steering/swift-patterns.md 展开。它是一份面向 AI 编程助手(Claude Code / Codex / Kiro 等)的“方向性规则”,用于约束、引导 Agent 在与
*.swift文件交互时遵循一套高质量的 Swift 设计惯例。读完本文,你将理解这套惯例在仓库内的完整落点:它不仅存在于 steering 文件本身,还以 rules/swift/patterns.md(规则镜像)、两个可直接复用的技能swift-actor-persistence与swift-protocol-di-testing,以及swift-reviewer评审 Agent 的形式贯穿整个仓库,从而形成“写码有模板、评审有标准、测试有方法”的闭环。
1. 这份文档在仓库中扮演什么角色
在 ECC 仓库中,.kiro/目录是面向 Kiro(一个 IDE 端 Agent 运行环境)的一套可安装组件集合,其 README 将其定义为“Bring Everything Claude Code (ECC) workflows to Kiro”。.kiro/steering/下共有 22 个 steering 文件,分别面向 dev、review、research 等不同模式以及各语言子集,而swift-patterns.md专门约束 Swift 代码的书写方式。
该文件的 front matter 给出了它的生效条件:
inclusion: fileMatch fileMatchPattern: "*.swift" description: Swift-specific patterns including protocol-oriented design, value types, actor pattern, and dependency injection即:凡是进入 Agent 上下文的*.swift文件,都会触发该 steering 规则的注入,使 Agent 在生成、修改、评审 Swift 代码时遵循其中定义的四种模式。
同时,这份内容并非孤本。在仓库根目录的 rules/swift/patterns.md 中存在内容一致的规则文件,其 front matter 将生效范围进一步明确为:
paths: - "**/*.swift" - "**/Package.swift"并注明“This file extends common/patterns.md with Swift specific content”——也就是说,rules/swift/与.kiro/steering/是同一套语言策略在不同 Agent 平台(Claude Code 风格 rules 与 Kiro 风格 steering)下的两种分发形态。围绕它展开的,还有 rules/swift/coding-style.md(编码风格)、rules/swift/testing.md(测试规范)、rules/swift/security.md(安全要求)与 rules/swift/hooks.md(编辑后自动检查)。
下文按原文档的四个小节逐层展开,并引入仓库内对应技能与评审 Agent 的实现作为纵深依据。
2. 协议导向设计:小协议 + 协议扩展默认实现
原文档给出的第一条准则是“定义小而聚焦的协议,用协议扩展提供共享默认实现”,并给出了一个典型仓储接口:
protocol Repository: Sendable { associatedtype Item: Identifiable & Sendable func find(by id: Item.ID) async throws -> Item? func save(_ item: Item) async throws }注意这个接口的三个细节,它们共同体现了“面向协议设计 + Swift 并发安全”的叠加要求:
associatedtype Item: Identifiable & Sendable:仓储所管理的实体需要可被唯一标识(Identifiable),并且能安全地跨越并发隔离域传递(Sendable)。find(by:)与save(_:)都是async throws方法:读取与写入被建模为可能失败、可能耗时(涉及 I/O 或网络)的异步操作。- 协议自身标记为
Sendable:在 Swift 6 严格并发检查下,跨 actor 传递协议类型时需要满足Sendable。
从仓库中与之配套的协议示例看,这套“小协议”思想在 skills/swift-protocol-di-testing/SKILL.md 中被进一步细化为一套可落地的分工:每个协议只负责一类外部关注点,例如把“文件系统能力”拆成三个独立协议:
// 文件系统访问入口 public protocol FileSystemProviding: Sendable { func containerURL(for purpose: Purpose) -> URL? } // 文件读写操作 public protocol FileAccessorProviding: Sendable { func read(from url: URL) throws -> Data func write(_ data: Data, to url: URL) throws func fileExists(at url: URL) -> Bool } // 书签存储(例如沙盒化 App 的授权恢复) public protocol BookmarkStorageProviding: Sendable { func saveBookmark(_ data: Data, for key: String) throws func loadBookmark(for key: String) throws -> Data? }该技能文件明确把“不要创建涵盖所有外部访问的巨型协议”列为反模式,与 steering 文档“小而聚焦”的表述一一对应。设计时把握一条铁律:协议应当围绕行为(role)而非类型(type)命名,并尽量保持单一职责,这也会在评审阶段由swift-reviewerAgent 校验。
3. 值类型优先:struct 承载模型,enum 建模状态机
原文档对值类型的要求分为两条:
- 用 struct 表示数据传输对象(DTO)与模型;
- 用带关联值的 enum 建模互斥的状态。
对应的状态建模示例是:
enum LoadState<T: Sendable>: Sendable { case idle case loading case loaded(T) case failed(Error) }这个LoadState泛型枚举把“加载中视图模型的全部可能状态”收拢到一处:未开始(idle)、加载中(loading)、已成功携带数据(loaded(T))、已失败携带错误(failed(Error))。编译器因此可以穷尽匹配所有分支,杜绝遗漏状态导致 UI 显示不一致的 bug。而Sendable约束则保证该状态值可以安全地作为 actor 方法返回值或@Observable视图模型属性跨隔离域传递。
ECC 仓库对“值类型”的支持并不停留在写法层面,rules/swift/coding-style.md 进一步给出约束:
- 默认使用
let而非var,只有当编译器要求可变时才改用var; - 默认使用带值语义的
struct,只有当确实需要“身份/引用语义”(如类标识、生命周期管理)时才使用class。
将 enum 状态机 + struct 模型结合,就可以在视图层得到可预测、可测试的状态流转:由loading到loaded(data),或由loading到failed(error),UI 对每一种状态都有确定的渲染策略。
4. Actor 模式:用编译器保证代替手工同步
原文档的第三条准则是“用 actor 管理共享可变状态,而不是锁或派发队列”,其最小示例为:
actor Cache<Key: Hashable & Sendable, Value: Sendable> { private var storage: [Key: Value] = [:] func get(_ key: Key) -> Value? { storage[key] } func set(_ key: Key, value: Value) { storage[key] = value } }actor Cache的所有实例方法在 actor 隔离域内串行执行,storage字典被private封装,外部只能通过await cache.set(...)访问。从源码结构看,Key与Value都被约束为Sendable,这保证了跨隔离域传参是安全的。相比NSLock/DispatchQueue+class的做法,actor 方案把“数据竞争”从运行时错误升级为编译期错误——误写并发访问代码会直接无法通过编译。
4.1 纵深:从最小缓存到完整持久化仓储
如果仅停留在最小示例,读者很难评估 actor 在真实工程里的价值。为此,仓库配套技能 skills/swift-actor-persistence/SKILL.md 提供了从“内存缓存 + 文件落盘”到“与视图模型联动”的完整范式。它的核心是一个泛型 actor 仓储:
public actor LocalRepository<T: Codable & Identifiable> where T.ID == String { private var cache: [String: T] = [:] private let fileURL: URL public init(directory: URL = .documentsDirectory, filename: String = "data.json") { self.fileURL = directory.appendingPathComponent(filename) // 同步加载:actor 隔离尚未生效,允许在 init 中直接读文件 self.cache = Self.loadSynchronously(from: fileURL) } // MARK: - Public API public func save(_ item: T) throws { cache[item.id] = item try persistToFile() } public func delete(_ id: String) throws { cache[id] = nil try persistToFile() } public func find(by id: String) -> T? { cache[id] } public func loadAll() -> [T] { Array(cache.values) } // MARK: - Private private func persistToFile() throws { let data = try JSONEncoder().encode(Array(cache.values)) try data.write(to: fileURL, options: .atomic) } private static func loadSynchronously(from url: URL) -> [String: T] { guard let data = try? Data(contentsOf: url), let items = try? JSONDecoder().decode([T].self, from: data) else { return [:] } return Dictionary(uniqueKeysWithValues: items.map { ($0.id, $0) }) } }这一实现把 steering 文档的抽象原则落地为工程决策,其“关键设计决策表”可帮助我们理解每个取舍:
| 决策 | 理由 |
|---|---|
| 用 actor(而非 class + 锁) | 编译期强制线程安全,无需手工同步 |
| 内存缓存 + 文件持久化 | 读走缓存快、写落盘可靠 |
| init 中同步加载 | 规避异步初始化带来的复杂度 |
| 以 ID 为键的字典 | 支持 O(1) 的按标识查找 |
泛型约束Codable & Identifiable | 任意模型类型均可复用 |
原子写入(.atomic) | 崩溃时避免产生半截文件 |
使用方注意:由于 actor 隔离,所有对外方法调用都必须await,例如:
let repository = LocalRepository<Question>() // 读:走内存缓存,O(1) let question = await repository.find(by: "q-001") let allQuestions = await repository.loadAll() // 写:更新缓存并原子落盘 try await repository.save(newQuestion) try await repository.delete("q-001")若与 SwiftUI 的@Observable视图模型配合,就可以把 actor 仓储注入 ViewModel,实现“视图操作 → actor 落盘 → 刷新列表”的响应式链路:
@Observable final class QuestionListViewModel { private(set) var questions: [Question] = [] private let repository: LocalRepository<Question> init(repository: LocalRepository<Question> = LocalRepository()) { self.repository = repository } func load() async { questions = await repository.loadAll() } func add(_ question: Question) async throws { try await repository.save(question) questions = await repository.loadAll() } }4.2 actor 的实用边界与反模式
skills/swift-actor-persistence/SKILL.md 同时给出最佳实践与反模式,可作为 steering 文档的补充判据:
最佳实践:
- 所有跨越 actor 边界的数据都应是
Sendable类型; - 保持 actor 公共 API 最小——只暴露领域操作,不暴露持久化细节;
- 文件写入一律
.atomic,防止写入中途崩溃损坏数据; - 本地小文件在
init中同步加载,避免异步初始化带来的复杂度; - 与
@ObservableViewModel 结合以获得响应式 UI 更新。
反模式:
- 在新 Swift 并发代码中继续使用
DispatchQueue/NSLock代替 actor; - 把内部缓存字典直接暴露给外部调用者;
- 忘记 actor 所有方法调用都是
await; - 用
nonisolated绕过 actor 隔离(等于自废武功)。
5. 依赖注入:默认参数注入协议,生产用默认、测试注 Mock
原文档的第四条准则给出了一个精炼的“默认参数注入”模式:
struct UserService { private let repository: any UserRepository init(repository: any UserRepository = DefaultUserRepository()) { self.repository = repository } }要点有两点:
- 面向协议编程:属性类型是协议
any UserRepository,而非具体类型DefaultUserRepository; - 默认参数实现“可选的注入”:生产环境不传参即可获得真实实现;测试环境传入 Mock 即可替换依赖,无需修改
UserService本身的代码,也无需依赖#if DEBUG之类条件编译。
5.1 纵深:一套完整的“协议 + 默认实现 + Mock”装配
原文档只给了结构示例,仓库配套技能 skills/swift-protocol-di-testing/SKILL.md 则给出了完整的四步装配法,适合用于文件系统、网络、iCloud 等外部依赖的抽象:
第一步:定义小而聚焦的协议(见上文FileSystemProviding/FileAccessorProviding/BookmarkStorageProviding)。
第二步:提供生产默认实现:
public struct DefaultFileSystemProvider: FileSystemProviding { public init() {} public func containerURL(for purpose: Purpose) -> URL? { FileManager.default.url(forUbiquityContainerIdentifier: nil) } } public struct DefaultFileAccessor: FileAccessorProviding { public init() {} public func read(from url: URL) throws -> Data { try Data(contentsOf: url) } public func write(_ data: Data, to url: URL) throws { try data.write(to: url, options: .atomic) } public func fileExists(at url: URL) -> Bool { FileManager.default.fileExists(atPath: url.path) } }第三步:为测试创建带“可注入错误”的 Mock:
public final class MockFileAccessor: FileAccessorProviding, @unchecked Sendable { public var files: [URL: Data] = [:] public var readError: Error? public var writeError: Error? public init() {} public func read(from url: URL) throws -> Data { if let error = readError { throw error } guard let data = files[url] else { throw CocoaError(.fileReadNoSuchFile) } return data } public func write(_ data: Data, to url: URL) throws { if let error = writeError { throw error } files[url] = data } public func fileExists(at url: URL) -> Bool { files[url] != nil } }Mock 的关键设计是readError/writeError两个可配置错误属性——它让“磁盘损坏、文件缺失、写入失败”等难以在真实环境触发的分支可以被确定性测试覆盖。
第四步:生产对象使用默认参数,测试注入 Mock:
public actor SyncManager { private let fileSystem: FileSystemProviding private let fileAccessor: FileAccessorProviding public init( fileSystem: FileSystemProviding = DefaultFileSystemProvider(), fileAccessor: FileAccessorProviding = DefaultFileAccessor() ) { self.fileSystem = fileSystem self.fileAccessor = fileAccessor } public func sync() async throws { guard let containerURL = fileSystem.containerURL(for: .sync) else { throw SyncError.containerNotAvailable } let data = try fileAccessor.read( from: containerURL.appendingPathComponent("data.json") ) // Process data... } }该技能文件总结的反模式同样值得注意:创建覆盖一切外部访问的“巨型协议”、给没有外部依赖的内部类型做 Mock、用#if DEBUG替代依赖注入、与 actor 连用时遗漏Sendable约束、以及过度设计——若某类型本无外部依赖,就根本不需要为它定义协议。
5.2 与 Swift Testing 结合:DI 的最终目的
DI 的最终目的是让业务逻辑在无 I/O 环境下被确定性测试。仓库 rules/swift/testing.md 明确规定:新测试统一使用Swift Testing(import Testing),配合@Test与#expect。例如验证创建邮箱非法的用户会被拒绝:
@Test("User creation validates email") func userCreationValidatesEmail() throws { #expect(throws: ValidationError.invalidEmail) { try User(email: "not-an-email") } }在 skills/swift-protocol-di-testing/SKILL.md 中,可以看到 Mock 注入 + Swift Testing 对错误分支的覆盖样例:
import Testing @Test("Sync manager handles missing container") func testMissingContainer() async { let mockFileSystem = MockFileSystemProvider(containerURL: nil) let manager = SyncManager(fileSystem: mockFileSystem) await #expect(throws: SyncError.containerNotAvailable) { try await manager.sync() } } @Test("Sync manager reads data correctly") func testReadData() async throws { let mockFileAccessor = MockFileAccessor() mockFileAccessor.files[testURL] = testData let manager = SyncManager(fileAccessor: mockFileAccessor) let result = try await manager.loadData() #expect(result == expectedData) } @Test("Sync manager handles read errors gracefully") func testReadError() async { let mockFileAccessor = MockFileAccessor() mockFileAccessor.readError = CocoaError(.fileReadCorruptFile) let manager = SyncManager(fileAccessor: mockFileAccessor) await #expect(throws: SyncError.self) { try await manager.sync() } }同时 rules/swift/testing.md 还要求每个测试获得全新实例(init中准备、deinit中清理,测试间不共享可变状态),并支持参数化测试:
@Test("Validates formats", arguments: ["json", "xml", "csv"]) func validatesFormat(format: String) throws { let parser = try Parser(format: format) #expect(parser.isValid) }覆盖率统计可通过命令执行:
swift test --enable-code-coverage6. 与并发、安全、工具链的配套约束
四种核心模式之外,同一套策略还通过 rules/swift/coding-style.md、rules/swift/security.md 与 rules/swift/hooks.md 约束 Swift 代码的其余维度,它们共同支撑 steering 文档中“Sendable 值类型 + actor + 面向协议”的三条并发主线:
- 严格并发:开启 Swift 6 严格并发检查;优先用
Sendable值类型穿越隔离域、用 actor 管理共享可变状态、用结构化并发(async let、TaskGroup)而非无结构Task {}。 - 类型化错误:Swift 6+ 使用
throws(LoadError)类型化抛出与模式匹配,而不是在catch里做字符串级判断。 - 格式化与风格:优先
let、默认struct,命名遵循 Apple API Design Guidelines,常量用static let;工具上推荐 SwiftFormat 自动格式化、SwiftLint 强制风格,Xcode 16+ 自带的swift-format亦可替代。 - 安全基线:敏感信息(token、密码、密钥)一律存入 Keychain,严禁放
UserDefaults,严禁硬编码在源码里(反编译可轻易提取);默认启用 App Transport Security,不得随意关闭;用户输入与外部数据(API、Deep Link、剪贴板)使用前必须校验。对应示例:
let apiKey = ProcessInfo.processInfo.environment["API_KEY"] guard let apiKey, !apiKey.isEmpty else { fatalError("API_KEY not configured") }- 编辑钩子:在
~/.claude/settings.json等平台配置中注册 PostToolUse 钩子,让 Agent 编辑.swift文件后自动执行 SwiftFormat、SwiftLint 与swift build类型检查,同时把生产代码中的print()标记出来(建议改用os.Logger或结构化日志)。
7. 在评审工作流中的应用:swift-reviewer Agent
steering 文件的价值最终要落到“有人照章执行、有人照章审查”。仓库为此提供了专职评审 Agent agents/swift-reviewer.md(在.kiro/agents/下也有对应版本),其定位描述与 steering 文档高度一致:专门评审“protocol-oriented design、value semantics、ARC memory management、Swift Concurrency 与惯用模式”,并要求所有 Swift 项目必须使用。
从该 Agent 的定义可以看到一套标准评审流程:
- 先执行
swift build、swiftlint lint --quiet、swift test,任一失败即停止并报告; - 用
git diff HEAD~1 -- '*.swift'(PR 评审时用git diff main...HEAD -- '*.swift')定位改动范围; - 聚焦修改过的
.swift文件展开评审。
其“CRITICAL - Safety”清单也呼应了上述安全规则:禁止生产路径上的强制解包(!)、无理由的try!与as!,禁止硬编码密钥,禁止把敏感数据写入UserDefaults,禁止无理由关闭 ATS,警惕 SQL/命令注入、路径穿越与不安全的反序列化。协议设计、值语义与并发正确性则在后续评审优先级中逐项核验。
可以这样理解整个体系的分工:steering 文件负责在写码前“给定范式”,rules 细化风格、测试、安全与钩子,skills 提供可直接复用的完整实现模板,swift-reviewer Agent 负责在评审时“按图索骥”。四个环节共享同一套 Swift 设计价值观。
8. 小结
.kiro/steering/swift-patterns.md用极简篇幅给出了四把“钥匙”,而 ECC 仓库用一整条技能/规则/Agent 链条把钥匙锻造成了完整工具:
| Steering 规则 | 核心要求 | 仓库配套纵深 |
|---|---|---|
| 协议导向设计 | 小协议 + 关联类型 + 协议扩展默认实现 | swift-protocol-di-testing 技能 中的 FileSystem/FileAccessor 协议族 |
| 值类型 | struct 做 DTO/模型,带关联值 enum 建模状态 | LoadState<T>状态机与 coding-style 的let/struct偏好 |
| Actor 模式 | 共享可变状态交给 actor,告别锁与派发队列 | swift-actor-persistence 技能 的LocalRepository完整实现 |
| 依赖注入 | 默认参数注入协议,测试注入 Mock | DefaultFileAccessor+MockFileAccessor+ Swift Testing 样例 |
对开发者而言,最直接的可操作建议是:新写.swift代码时,把本文第 2~5 节的四个模板作为起点;涉及持久化时直接参考 skills/swift-actor-persistence/SKILL.md,涉及文件/网络等外部依赖的测试化改造时参考 skills/swift-protocol-di-testing/SKILL.md;提交前用swift test --enable-code-coverage校验覆盖,并允许swift-reviewerAgent 以 agents/swift-reviewer.md 定义的安全与风格清单完成终审。
相关仓库路径速查:
- 本文主体规则:.kiro/steering/swift-patterns.md、镜像 rules/swift/patterns.md
- 配套语言规则:rules/swift/coding-style.md、rules/swift/testing.md、rules/swift/security.md、rules/swift/hooks.md
- 纵深技能:skills/swift-actor-persistence/SKILL.md、skills/swift-protocol-di-testing/SKILL.md
- 评审 Agent:agents/swift-reviewer.md(
.kiro版本位于 .kiro/agents/swift-reviewer.md) - 分发说明:.kiro/README.md
【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考