swift
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwift
Swift
Use this skill for Swift code that needs strong API boundaries, predictable threading, and idiomatic Apple-platform integration. When the code is part of a Nitro Module, pair this with for generated specs, Promise mapping, and HybridObject constraints.
build-nitro-modules本技能适用于满足以下需求的Swift代码:清晰的API边界、可预测的线程模型,以及符合Apple平台原生规范的集成方式。如果代码属于Nitro Module的一部分,请结合技能使用,以处理生成的规范、Promise映射和HybridObject约束。
build-nitro-modulesWorkflow
工作流
- Read the local Swift code, generated protocols, and surrounding API shape before editing.
- Choose the public type model first: make invalid states unrepresentable where Swift can express them.
- Choose one concurrency model for the feature before writing implementation code.
- Keep synchronous properties and methods cheap, local, and nonblocking.
- Make queue hops, hardware/session negotiation, I/O, and fallible async work explicit in the API.
- 编辑前先阅读本地Swift代码、生成的协议及周边API结构。
- 优先确定公共类型模型:在Swift可表达的范围内,让无效状态无法被表示。
- 编写实现代码前,先为该功能选定一种并发模型。
- 同步属性和方法应保持轻量、局部且非阻塞。
- 在API中显式体现队列切换、硬件/会话协商、I/O操作及可能失败的异步工作。
Type-Safe API Design
类型安全的API设计
- Represent state variants with types, not nullable clusters. Use protocols plus conforming structs/classes when variants share a public contract, or use with associated values when the set is closed and value-like.
enum - Keep related fields nonoptional on the same variant. If and
barcodeare meaningful only together, put both onbarcodeType; do not make both optional on a generic scanned-data struct.ScannedBarcode - Use optionals only for real domain absence inside one state, not for expressing which state the object is in.
- Prefer compile-time flow over caller-side probing. If callers need repeated chains to discover valid field combinations, the API shape is probably wrong.
if let
swift
// Avoid: the valid combinations are implicit and easy to misuse.
struct ScannedData {
let position: Point
let text: String?
let barcode: String?
let barcodeType: BarcodeType?
let face: Rect?
}
// Prefer: each state exposes the fields that are valid for that state.
protocol ScannedData {
var position: Point { get }
}
struct ScannedText: ScannedData {
let position: Point
let text: String
}
struct ScannedBarcode: ScannedData {
let position: Point
let barcode: String
let barcodeType: BarcodeType
}
struct ScannedFace: ScannedData {
let position: Point
let face: Rect
}- 用类型表示状态变体,而非一堆可空值。当变体共享公共契约时,使用协议加上遵循该协议的结构体/类;当变体集合是封闭且值类型的,使用带关联值的。
enum - 将相关字段放在同一个变体中且设为非可选。如果和
barcode只有同时存在才有意义,就把它们都放在barcodeType中;不要在通用的扫描数据结构体里把它们都设为可选。ScannedBarcode - 仅在单个状态中表示真实的业务层面缺失时才使用可选值,不要用可选值来表示对象所处的状态。
- 优先采用编译期流程控制,而非让调用方自行探测。如果调用方需要反复使用链来找出有效的字段组合,那API结构大概率是不合理的。
if let
swift
// Avoid: the valid combinations are implicit and easy to misuse.
struct ScannedData {
let position: Point
let text: String?
let barcode: String?
let barcodeType: BarcodeType?
let face: Rect?
}
// Prefer: each state exposes the fields that are valid for that state.
protocol ScannedData {
var position: Point { get }
}
struct ScannedText: ScannedData {
let position: Point
let text: String
}
struct ScannedBarcode: ScannedData {
let position: Point
let barcode: String
let barcodeType: BarcodeType
}
struct ScannedFace: ScannedData {
let position: Point
let face: Rect
}Concurrency Model
并发模型
- Choose Swift concurrency or DispatchQueue for a feature, not both as interleaved control flow.
- Keep quick, deterministic, local work synchronous. Do not introduce ,
Task, or Promise plumbing for simple value construction, cached metadata, or pure transforms.DispatchQueue - Use Swift /
async,await, and actors only when the full operation can be represented cleanly in Swift concurrency without queue escape hatches.Task - Use a private owned serial when Apple APIs, delegates, callbacks, C++ bridges, JS runtimes, or Nitro thread boundaries already revolve around queues, or when heavier native work must not block the caller.
DispatchQueue - Avoid unless the platform API requires the main thread, such as UIKit/AppKit/VisionKit presentation or view mutation. Keep main-thread blocks small and move parsing, conversion, I/O, session negotiation, and CPU work to an owned queue or async API.
DispatchQueue.main - Treat repeated ,
Task, actor, or thread hops as an architecture smell. A component should either own the queue/actor it works on, or cross into that owner once at the public async boundary or native callback boundary.DispatchQueue - If a workflow bounces between main, background, JS, and native queues in multiple nested places, stop and redesign the object/lifecycle/API. Excessive hops hide latency, make ordering harder to reason about, and create future performance problems.
- Do not fix races or readiness bugs with ,
DispatchQueue.asyncAfter,Task.sleep, timers, extra queue hops, or calling the same method twice. Fix the owner queue, lifecycle state, completion callback, delegate event, or async API boundary instead. Retry only for external nondeterminism such as hardware, OS services, remote services, or network, and keep retries bounded, cancellable, and idempotent.Thread.sleep - Do not use as a generic main-thread hop from a nonisolated or Nitro-generated entry point. It creates unstructured Swift concurrency. Use it only when the operation is otherwise Swift-concurrency based and benefits from
Task { @MainActor in ... }/async, task cancellation, or actor isolation end to end.await - For UIKit, AppKit, VisionKit, and other main-thread callback/delegate APIs, prefer a direct boundary or an async public method that owns the hop. Direct
DispatchQueue.main.asyncis understood by Swift's actor checker forDispatchQueue.main.asynccalls; generic queue wrappers such as@MainActorusually are not.Promise.parallel(.main) - If a callback can return on an arbitrary queue, normalize it to the chosen owner queue once, close to the callback source, instead of scattering nested or
Task { @MainActor }hops through the workflow.DispatchQueue.main.async - Do not force ,
MainActor.assumeIsolatedbranches, or queueThread.isMainThreadcalls to make a Swift-concurrency design compile. That is a signal to choose a queue-based design or change the API boundary..sync - Do not call ,
DispatchQueue.sync, or equivalent synchronous queue hops. Treat them as bugs, especially in property getters and setters.DispatchQueue.main.sync - Use or a Nitro
DispatchQueue.asyncmethod for queue-bound work that callers must wait for.Promise - Treat locks as a last-resort synchronization primitive, not a default safety wrapper. Before adding , identify the concrete shared mutable values, the threads/queues that can access them concurrently, and why ownership by
NSLock, a serial queue, a Nitro runtime/thread, or immutable snapshots is not enough.MainActor - Swift and
Arraymutations are not thread-safe. If listener add/remove can race with native delegate emission, either serialize every access on one owner queue/thread, or use a tiny lock only to mutate and snapshot the listener registry.Dictionary - Never hold a lock while invoking callbacks, calling into JS/Nitro, or calling unknown user code. Snapshot listeners under the lock, unlock, then call them. A listener removed during an in-flight emission may receive that current event; that is acceptable cleanup semantics and does not justify heavier locking.
- 为一个功能选择Swift并发模型或DispatchQueue,不要将两者穿插在控制流中混合使用。
- 快速、确定的局部工作保持同步执行。不要为简单的值构造、缓存元数据或纯转换操作引入、
Task或Promise相关的基础设施。DispatchQueue - 仅当整个操作可以用Swift并发模型清晰表达且不需要队列逃逸机制时,才使用Swift的/
async、await和actors。Task - 当Apple API、委托、回调、C++桥接、JS运行时或Nitro线程边界本身就基于队列构建时,或是较重的原生工作不能阻塞调用方时,使用一个私有的串行。
DispatchQueue - 避免使用,除非平台API要求必须在主线程执行,比如UIKit/AppKit/VisionKit的展示或视图变更。保持主线程代码块短小,将解析、转换、I/O、会话协商和CPU密集型工作移到自有队列或异步API中执行。
DispatchQueue.main - 将反复的、
Task、actor或线程切换视为架构坏味道。一个组件要么拥有自己工作的队列/actor,要么在公共异步边界或原生回调边界处仅做一次跨域切换。DispatchQueue - 如果一个工作流在主线程、后台线程、JS和原生队列之间的多个嵌套位置来回切换,应当停下来重新设计对象/生命周期/API。过多的切换会隐藏延迟,让执行顺序更难推导,还会带来未来的性能问题。
- 不要用、
DispatchQueue.asyncAfter、Task.sleep、定时器、额外的队列切换或重复调用同一方法来修复竞态或就绪状态问题。应当修复所属队列、生命周期状态、完成回调、委托事件或异步API边界。仅在遇到外部非确定性因素(如硬件、操作系统服务、远程服务或网络)时才使用重试,且重试需有次数限制、可取消且具备幂等性。Thread.sleep - 不要将作为从非隔离或Nitro生成的入口点切换到主线程的通用方式。这会产生非结构化的Swift并发代码。仅当操作本身基于Swift并发模型,且能从端到端的
Task { @MainActor in ... }/async、任务取消或actor隔离中获益时才使用它。await - 对于UIKit、AppKit、VisionKit及其他主线程回调/委托API,优先使用直接的边界或负责切换的异步公共方法。Swift的actor检查器能识别
DispatchQueue.main.async调用中的直接@MainActor;但通用队列包装器(如DispatchQueue.main.async)通常无法被识别。Promise.parallel(.main) - 如果回调可能在任意队列上返回,应在靠近回调源的位置将其一次性统一到选定的所属队列,而不是在整个工作流中散落嵌套的或
Task { @MainActor }切换。DispatchQueue.main.async - 不要为了让Swift并发设计通过编译,而强行使用、
MainActor.assumeIsolated分支或队列的Thread.isMainThread调用。这是一个信号,说明应该选择基于队列的设计或修改API边界。.sync - 不要调用、
DispatchQueue.sync或等效的同步队列切换。应将其视为bug,尤其是在属性的getter和setter中。DispatchQueue.main.sync - 对于调用方必须等待的队列绑定工作,使用或Nitro的
DispatchQueue.async方法。Promise - 将锁作为最后的同步原语,而非默认的安全包装器。在添加之前,先明确具体的共享可变值、可能并发访问它们的线程/队列,以及为什么
NSLock、串行队列、Nitro运行时/线程或不可变快照的所有权机制不足以解决问题。MainActor - Swift的和
Array的修改操作不是线程安全的。如果监听器的添加/移除可能与原生委托的触发产生竞态,要么将所有访问都序列化到同一个所属队列/线程上,要么仅在修改和快照监听器注册表时使用轻量锁。Dictionary - 在调用回调、调用JS/Nitro或调用未知用户代码时,绝对不要持有锁。在持有锁时对监听器做快照,释放锁之后再调用它们。在事件触发过程中被移除的监听器可能会收到当前事件;这是可接受的清理语义,不需要使用更重的锁机制。
Properties and Threading
属性与线程
- Use properties only for cheap observed state that can be read or written immediately and safely.
- Do not hide a thread hop, hardware/session query, lock wait, permission check, allocation, or fallible native operation behind a property.
- If a value can only be observed from a specific queue, prefer an event/listener emitted from that queue.
- If a caller truly needs a one-shot read from another queue, expose an async getter method instead of a property.
- In Nitro Modules, represent queue-bound reads and writes as methods, usually with
Promise<T>.Promise.parallel(queue)
swift
// Avoid: synchronous queue hop hidden in a getter.
var status: SessionStatus {
queue.sync { session.status }
}
// Prefer: make the async boundary explicit.
func getStatus() throws -> Promise<SessionStatus> {
return Promise.parallel(queue) {
return self.session.status
}
}- 属性仅用于表示可立即安全读写的轻量可观测状态。
- 不要将线程切换、硬件/会话查询、锁等待、权限检查、内存分配或可能失败的原生操作隐藏在属性背后。
- 如果一个值只能从特定队列被观测,优先使用从该队列发出的事件/监听器。
- 如果调用方确实需要从另一个队列进行一次性读取,应提供异步getter方法而非属性。
- 在Nitro Module中,将队列绑定的读写操作表示为方法,通常配合
Promise<T>使用。Promise.parallel(queue)
swift
// Avoid: synchronous queue hop hidden in a getter.
var status: SessionStatus {
queue.sync { session.status }
}
// Prefer: make the async boundary explicit.
func getStatus() throws -> Promise<SessionStatus> {
return Promise.parallel(queue) {
return self.session.status
}
}Swift Style
Swift编码风格
- Make classes by default unless subclassing is part of the design.
final - Prefer Swift-native types such as ,
String,Array, structs, protocols, and Foundation value types. Avoid Objective-C bridge types unless an Apple API requires them.Dictionary - Use to validate input and state early. Throw specific errors for user-reachable failures.
guard - Treat a filename as a scope contract. should implement
HybridDataScanner.swift; it should not also contain extensions, conversions,HybridDataScannerhelpers, delegates, or framework adapters.UIViewController - Keep one top-level implementation type per file. Do not put secondary structs, classes, enums, protocols, option adapters, coordinators, delegates, or helper types below the primary type. If a helper deserves a type, it deserves its own file.
- Keep one focused extension/conversion per file. Do not collect multiple extensions in one utility file. Put reusable extensions in named files such as ,
Extensions/UIViewController+topPresentedViewController.swift, orExtensions/CGPoint+Point.swiftwithExtensions/Barcode+toScannedCode.swiftorinternalvisibility where appropriate.package - Never put Swift extensions inside implementation files or other primary implementation files, even when the extension is private, tiny, or only used by that file. Put every extension in a separate named
Hybrid*extension/converter file so code splitting, maintainability, and future diffs stay clean.Type+operation.swift - Split delegates, framework adapters, converters, native protocols, and helper state into separate files instead of adding them below the main type.
- Extract native preflight checks and platform boilerplate out of factories and implementation methods. Permission/status switches,
Hybrid*checks, hardware capability checks, Info.plist validation, and similar setup guards belong in focused helper/extension files such asBundle.main.object(forInfoDictionaryKey:)orAVCaptureDevice+CameraAuthorization.swift; the factory call site should stay one or two lines.Bundle+CameraUsageDescription.swift - Keep as orchestration only: resolve generated options, call preflight helpers, create/start the native session, and return/reject the Promise. Do not define session/coordinator/delegate classes,
Hybrid*Factory.swiftadapters, presenter traversal helpers, scanner configuration builders, barcode mappings, permission switches, or Info.plist checks in the factory file.Native*Options - If a method grows a switch over Apple authorization/status, a multi-line guard for platform setup, view-controller lookup logic,
Hybrid*configuration, or conversion loops, extract it before continuing. The caller should read as a short sequence of named operations, not as the implementation of each operation.DataScannerViewController - Do not create broad files for these helpers. Name the file after the platform type or domain check and keep each helper focused on one check or conversion.
Utils.swift - Use line count as a review signal: files below roughly 300 lines are usually fine only after the one-type-per-file and no-helpers-in-factory rules are satisfied. Size caused by extensions, conversions, helper types, or platform glue is a design issue.
- Prefer inline shorthand for unambiguous single-expression closures: , not a multi-line
targetFormats.flatMap { $0.toVNBarcodeFormat() }. Do not useflatMap { format in format.toVNBarcodeFormat() }when a surrounding or nested closure already uses shorthand arguments; name parameters in nested closures or when clarity needs it.$0 - Put conversions on the element type, not on arrays, when the conversion only reads one element. The element method may still return multiple values; compose callers with ,
map,flatMap, orreduce.Set(...) - Do not put domain conversions on broad/common receivers such as ,
Int,String,Double,Any, orCGPointunless the conversion is genuinely about that type. Prefer the domain type direction, such asCGRectorBarcodeFormat.from(format:), overBarcodeFormat(nativeFormat:).Int.toBarcodeFormat() - Add or
Arrayextensions only when the collection has real domain behavior, such as validation across elements, deduplication, ordering, batching, caching, nonempty checks, or error aggregation. If the receiver is a concreteCollectionand the body mostly saves one[SomeDomainType], keep that map in caller code.map - Break meaningful conversions into named intermediate values instead of long inline expressions.
- 默认将类声明为,除非子类化是设计的一部分。
final - 优先使用Swift原生类型,如、
String、Array、结构体、协议以及Foundation值类型。除非Apple API要求,否则避免使用Objective-C桥接类型。Dictionary - 使用尽早验证输入和状态。对于用户可感知的失败,抛出具体的错误。
guard - 将文件名视为作用域契约。应该只实现
HybridDataScanner.swift;不应同时包含扩展、转换、HybridDataScanner辅助方法、委托或框架适配器。UIViewController - 每个文件只保留一个顶层实现类型。不要在主类型下方放置次要的结构体、类、枚举、协议、选项适配器、协调器、委托或辅助类型。如果一个辅助逻辑值得单独成类型,它就值得拥有自己的文件。
- 每个文件只包含一个职责单一的扩展/转换。不要在一个工具文件中收集多个扩展。将可复用的扩展放在命名文件中,例如、
Extensions/UIViewController+topPresentedViewController.swift或Extensions/CGPoint+Point.swift,并在合适的情况下使用Extensions/Barcode+toScannedCode.swift或internal可见性。package - 绝对不要将Swift扩展放在实现文件或其他主要实现文件中,即使该扩展是私有的、很小或仅被该文件使用。将每个扩展都放在单独的命名为
Hybrid*的扩展/转换器文件中,以保持代码拆分、可维护性和未来的diff清晰。Type+operation.swift - 将委托、框架适配器、转换器、原生协议和辅助状态拆分到单独的文件中,而不是加在主类型下方。
- 将原生前置检查和平台样板代码从工厂和实现方法中提取出来。权限/状态开关、
Hybrid*检查、硬件能力检查、Info.plist验证及类似的设置守卫应该放在职责单一的辅助/扩展文件中,例如Bundle.main.object(forInfoDictionaryKey:)或AVCaptureDevice+CameraAuthorization.swift;工厂调用处应保持一两行的长度。Bundle+CameraUsageDescription.swift - 仅负责编排工作:解析生成的选项、调用前置检查辅助方法、创建/启动原生会话,以及返回/拒绝Promise。不要在工厂文件中定义会话/协调器/委托类、
Hybrid*Factory.swift适配器、presenter遍历辅助方法、扫描器配置构建器、条码映射、权限开关或Info.plist检查。Native*Options - 如果方法中出现了关于Apple授权/状态的switch、多行的平台设置guard、视图控制器查找逻辑、
Hybrid*配置或转换循环,先将其提取出来再继续开发。调用方读起来应该是一系列简短的命名操作,而不是每个操作的具体实现。DataScannerViewController - 不要为这些辅助方法创建宽泛的文件。用平台类型或业务检查来命名文件,让每个辅助方法都专注于一项检查或转换。
Utils.swift - 将行数作为评审参考:只有在满足“每个文件一个类型”和“工厂中不放辅助方法”的规则后,文件大约300行以内才是合理的。由扩展、转换、辅助类型或平台粘合代码导致的文件过大是设计问题。
- 对于含义明确的单表达式闭包,优先使用内联简写形式:,而不是多行的
targetFormats.flatMap { $0.toVNBarcodeFormat() }。当外层或嵌套闭包已经使用了简写参数时,不要使用flatMap { format in format.toVNBarcodeFormat() };在嵌套闭包中或为了清晰起见,应命名参数。$0 - 当转换仅读取单个元素时,将转换方法放在元素类型上,而不是数组上。元素方法仍然可以返回多个值;调用方通过、
map、flatMap或reduce来组合使用。Set(...) - 不要将业务转换放在宽泛/通用的接收者类型上,如、
Int、String、Double、Any或CGPoint,除非该转换确实是关于该类型本身的。优先采用业务类型方向的写法,例如CGRect或BarcodeFormat.from(format:),而非BarcodeFormat(nativeFormat:)。Int.toBarcodeFormat() - 仅当集合具有真实的业务行为时,才添加或
Array扩展,例如跨元素验证、去重、排序、批处理、缓存、非空检查或错误聚合。如果接收者是具体的Collection且扩展体主要只是省了一个[SomeDomainType]调用,那就把那个map留在调用方代码里。map - 将有意义的转换拆分为命名的中间值,而非使用冗长的内联表达式。
Nitro Notes
Nitro注意事项
- Use for DispatchQueue-based work such as AVFoundation session operations.
Promise.parallel(queue) - Use only when wrapping Swift
Promise.async/asyncor Task-native APIs end to end.await - Avoid manually creating or passing around instances. Prefer
Promise<T>,Promise.parallel,Promise.async, andPromise.resolved; use a manual Promise only for real native completion/delegate/callback bridges and keep it in the smallest scope with exactly-once completion.Promise.rejected - Do not mix with queue
Promise.asynccalls or actor escape hatches..sync - Generated HybridObject properties are synchronous entry points. Redesign them as methods or listeners if they need queue affinity.
- 对于基于DispatchQueue的工作(如AVFoundation会话操作),使用。
Promise.parallel(queue) - 仅当端到端包装Swift /
async或Task原生API时,才使用await。Promise.async - 避免手动创建或传递实例。优先使用
Promise<T>、Promise.parallel、Promise.async和Promise.resolved;仅在对接真实的原生完成/委托/回调时才使用手动Promise,并将其限制在最小作用域内,确保仅完成一次。Promise.rejected - 不要将与队列的
Promise.async调用或actor逃逸机制混合使用。.sync - 生成的HybridObject属性是同步入口点。如果它们需要队列亲和性,应将其重新设计为方法或监听器。