swift

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Swift

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
build-nitro-modules
for generated specs, Promise mapping, and HybridObject constraints.
本技能适用于满足以下需求的Swift代码:清晰的API边界、可预测的线程模型,以及符合Apple平台原生规范的集成方式。如果代码属于Nitro Module的一部分,请结合
build-nitro-modules
技能使用,以处理生成的规范、Promise映射和HybridObject约束。

Workflow

工作流

  1. Read the local Swift code, generated protocols, and surrounding API shape before editing.
  2. Choose the public type model first: make invalid states unrepresentable where Swift can express them.
  3. Choose one concurrency model for the feature before writing implementation code.
  4. Keep synchronous properties and methods cheap, local, and nonblocking.
  5. Make queue hops, hardware/session negotiation, I/O, and fallible async work explicit in the API.
  1. 编辑前先阅读本地Swift代码、生成的协议及周边API结构。
  2. 优先确定公共类型模型:在Swift可表达的范围内,让无效状态无法被表示。
  3. 编写实现代码前,先为该功能选定一种并发模型。
  4. 同步属性和方法应保持轻量、局部且非阻塞。
  5. 在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
    enum
    with associated values when the set is closed and value-like.
  • Keep related fields nonoptional on the same variant. If
    barcode
    and
    barcodeType
    are meaningful only together, put both on
    ScannedBarcode
    ; do not make both optional on a generic scanned-data struct.
  • 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
    if let
    chains to discover valid field combinations, the API shape is probably wrong.
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
    中;不要在通用的扫描数据结构体里把它们都设为可选。
  • 仅在单个状态中表示真实的业务层面缺失时才使用可选值,不要用可选值来表示对象所处的状态。
  • 优先采用编译期流程控制,而非让调用方自行探测。如果调用方需要反复使用
    if let
    链来找出有效的字段组合,那API结构大概率是不合理的。
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
    ,
    DispatchQueue
    , or Promise plumbing for simple value construction, cached metadata, or pure transforms.
  • Use Swift
    async
    /
    await
    ,
    Task
    , and actors only when the full operation can be represented cleanly in Swift concurrency without queue escape hatches.
  • Use a private owned serial
    DispatchQueue
    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.
  • Avoid
    DispatchQueue.main
    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.
  • Treat repeated
    Task
    ,
    DispatchQueue
    , 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.
  • 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
    ,
    Thread.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.
  • Do not use
    Task { @MainActor in ... }
    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
    async
    /
    await
    , task cancellation, or actor isolation end to end.
  • For UIKit, AppKit, VisionKit, and other main-thread callback/delegate APIs, prefer a direct
    DispatchQueue.main.async
    boundary or an async public method that owns the hop. Direct
    DispatchQueue.main.async
    is understood by Swift's actor checker for
    @MainActor
    calls; generic queue wrappers such as
    Promise.parallel(.main)
    usually are not.
  • 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
    Task { @MainActor }
    or
    DispatchQueue.main.async
    hops through the workflow.
  • Do not force
    MainActor.assumeIsolated
    ,
    Thread.isMainThread
    branches, or queue
    .sync
    calls to make a Swift-concurrency design compile. That is a signal to choose a queue-based design or change the API boundary.
  • Do not call
    DispatchQueue.sync
    ,
    DispatchQueue.main.sync
    , or equivalent synchronous queue hops. Treat them as bugs, especially in property getters and setters.
  • Use
    DispatchQueue.async
    or a Nitro
    Promise
    method for queue-bound work that callers must wait for.
  • Treat locks as a last-resort synchronization primitive, not a default safety wrapper. Before adding
    NSLock
    , identify the concrete shared mutable values, the threads/queues that can access them concurrently, and why ownership by
    MainActor
    , a serial queue, a Nitro runtime/thread, or immutable snapshots is not enough.
  • Swift
    Array
    and
    Dictionary
    mutations 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.
  • 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
    、
    DispatchQueue
    或Promise相关的基础设施。
  • 仅当整个操作可以用Swift并发模型清晰表达且不需要队列逃逸机制时,才使用Swift的
    async
    /
    await
    、
    Task
    和actors。
  • 当Apple API、委托、回调、C++桥接、JS运行时或Nitro线程边界本身就基于队列构建时,或是较重的原生工作不能阻塞调用方时,使用一个私有的串行
    DispatchQueue
    。
  • 避免使用
    DispatchQueue.main
    ,除非平台API要求必须在主线程执行,比如UIKit/AppKit/VisionKit的展示或视图变更。保持主线程代码块短小,将解析、转换、I/O、会话协商和CPU密集型工作移到自有队列或异步API中执行。
  • 将反复的
    Task
    、
    DispatchQueue
    、actor或线程切换视为架构坏味道。一个组件要么拥有自己工作的队列/actor,要么在公共异步边界或原生回调边界处仅做一次跨域切换。
  • 如果一个工作流在主线程、后台线程、JS和原生队列之间的多个嵌套位置来回切换,应当停下来重新设计对象/生命周期/API。过多的切换会隐藏延迟,让执行顺序更难推导,还会带来未来的性能问题。
  • 不要用
    DispatchQueue.asyncAfter
    、
    Task.sleep
    、
    Thread.sleep
    、定时器、额外的队列切换或重复调用同一方法来修复竞态或就绪状态问题。应当修复所属队列、生命周期状态、完成回调、委托事件或异步API边界。仅在遇到外部非确定性因素(如硬件、操作系统服务、远程服务或网络)时才使用重试,且重试需有次数限制、可取消且具备幂等性。
  • 不要将
    Task { @MainActor in ... }
    作为从非隔离或Nitro生成的入口点切换到主线程的通用方式。这会产生非结构化的Swift并发代码。仅当操作本身基于Swift并发模型,且能从端到端的
    async
    /
    await
    、任务取消或actor隔离中获益时才使用它。
  • 对于UIKit、AppKit、VisionKit及其他主线程回调/委托API,优先使用直接的
    DispatchQueue.main.async
    边界或负责切换的异步公共方法。Swift的actor检查器能识别
    @MainActor
    调用中的直接
    DispatchQueue.main.async
    ;但通用队列包装器(如
    Promise.parallel(.main)
    )通常无法被识别。
  • 如果回调可能在任意队列上返回,应在靠近回调源的位置将其一次性统一到选定的所属队列,而不是在整个工作流中散落嵌套的
    Task { @MainActor }
    或
    DispatchQueue.main.async
    切换。
  • 不要为了让Swift并发设计通过编译,而强行使用
    MainActor.assumeIsolated
    、
    Thread.isMainThread
    分支或队列的
    .sync
    调用。这是一个信号,说明应该选择基于队列的设计或修改API边界。
  • 不要调用
    DispatchQueue.sync
    、
    DispatchQueue.main.sync
    或等效的同步队列切换。应将其视为bug,尤其是在属性的getter和setter中。
  • 对于调用方必须等待的队列绑定工作,使用
    DispatchQueue.async
    或Nitro的
    Promise
    方法。
  • 将锁作为最后的同步原语,而非默认的安全包装器。在添加
    NSLock
    之前,先明确具体的共享可变值、可能并发访问它们的线程/队列,以及为什么
    MainActor
    、串行队列、Nitro运行时/线程或不可变快照的所有权机制不足以解决问题。
  • 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
    Promise<T>
    methods, usually with
    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
    final
    by default unless subclassing is part of the design.
  • Prefer Swift-native types such as
    String
    ,
    Array
    ,
    Dictionary
    , structs, protocols, and Foundation value types. Avoid Objective-C bridge types unless an Apple API requires them.
  • Use
    guard
    to validate input and state early. Throw specific errors for user-reachable failures.
  • Treat a filename as a scope contract.
    HybridDataScanner.swift
    should implement
    HybridDataScanner
    ; it should not also contain extensions, conversions,
    UIViewController
    helpers, delegates, or framework adapters.
  • 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
    ,
    Extensions/CGPoint+Point.swift
    , or
    Extensions/Barcode+toScannedCode.swift
    with
    internal
    or
    package
    visibility where appropriate.
  • Never put Swift extensions inside
    Hybrid*
    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
    Type+operation.swift
    extension/converter file so code splitting, maintainability, and future diffs stay clean.
  • 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
    Hybrid*
    factories and implementation methods. Permission/status switches,
    Bundle.main.object(forInfoDictionaryKey:)
    checks, hardware capability checks, Info.plist validation, and similar setup guards belong in focused helper/extension files such as
    AVCaptureDevice+CameraAuthorization.swift
    or
    Bundle+CameraUsageDescription.swift
    ; the factory call site should stay one or two lines.
  • Keep
    Hybrid*Factory.swift
    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,
    Native*Options
    adapters, presenter traversal helpers, scanner configuration builders, barcode mappings, permission switches, or Info.plist checks in the factory file.
  • If a
    Hybrid*
    method grows a switch over Apple authorization/status, a multi-line guard for platform setup, view-controller lookup logic,
    DataScannerViewController
    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.
  • Do not create broad
    Utils.swift
    files for these helpers. Name the file after the platform type or domain check and keep each helper focused on one check or conversion.
  • 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:
    targetFormats.flatMap { $0.toVNBarcodeFormat() }
    , not a multi-line
    flatMap { format in format.toVNBarcodeFormat() }
    . Do not use
    $0
    when a surrounding or nested closure already uses shorthand arguments; name parameters in nested closures or when clarity needs it.
  • 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
    ,
    reduce
    , or
    Set(...)
    .
  • Do not put domain conversions on broad/common receivers such as
    Int
    ,
    String
    ,
    Double
    ,
    Any
    ,
    CGPoint
    , or
    CGRect
    unless the conversion is genuinely about that type. Prefer the domain type direction, such as
    BarcodeFormat.from(format:)
    or
    BarcodeFormat(nativeFormat:)
    , over
    Int.toBarcodeFormat()
    .
  • Add
    Array
    or
    Collection
    extensions 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 concrete
    [SomeDomainType]
    and the body mostly saves one
    map
    , keep that map in caller code.
  • Break meaningful conversions into named intermediate values instead of long inline expressions.
  • 默认将类声明为
    final
    ,除非子类化是设计的一部分。
  • 优先使用Swift原生类型,如
    String
    、
    Array
    、
    Dictionary
    、结构体、协议以及Foundation值类型。除非Apple API要求,否则避免使用Objective-C桥接类型。
  • 使用
    guard
    尽早验证输入和状态。对于用户可感知的失败,抛出具体的错误。
  • 将文件名视为作用域契约。
    HybridDataScanner.swift
    应该只实现
    HybridDataScanner
    ;不应同时包含扩展、转换、
    UIViewController
    辅助方法、委托或框架适配器。
  • 每个文件只保留一个顶层实现类型。不要在主类型下方放置次要的结构体、类、枚举、协议、选项适配器、协调器、委托或辅助类型。如果一个辅助逻辑值得单独成类型,它就值得拥有自己的文件。
  • 每个文件只包含一个职责单一的扩展/转换。不要在一个工具文件中收集多个扩展。将可复用的扩展放在命名文件中,例如
    Extensions/UIViewController+topPresentedViewController.swift
    、
    Extensions/CGPoint+Point.swift
    或
    Extensions/Barcode+toScannedCode.swift
    ,并在合适的情况下使用
    internal
    或
    package
    可见性。
  • 绝对不要将Swift扩展放在
    Hybrid*
    实现文件或其他主要实现文件中,即使该扩展是私有的、很小或仅被该文件使用。将每个扩展都放在单独的命名为
    Type+operation.swift
    的扩展/转换器文件中,以保持代码拆分、可维护性和未来的diff清晰。
  • 将委托、框架适配器、转换器、原生协议和辅助状态拆分到单独的文件中,而不是加在主类型下方。
  • 将原生前置检查和平台样板代码从
    Hybrid*
    工厂和实现方法中提取出来。权限/状态开关、
    Bundle.main.object(forInfoDictionaryKey:)
    检查、硬件能力检查、Info.plist验证及类似的设置守卫应该放在职责单一的辅助/扩展文件中,例如
    AVCaptureDevice+CameraAuthorization.swift
    或
    Bundle+CameraUsageDescription.swift
    ;工厂调用处应保持一两行的长度。
  • Hybrid*Factory.swift
    仅负责编排工作:解析生成的选项、调用前置检查辅助方法、创建/启动原生会话,以及返回/拒绝Promise。不要在工厂文件中定义会话/协调器/委托类、
    Native*Options
    适配器、presenter遍历辅助方法、扫描器配置构建器、条码映射、权限开关或Info.plist检查。
  • 如果
    Hybrid*
    方法中出现了关于Apple授权/状态的switch、多行的平台设置guard、视图控制器查找逻辑、
    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
    Promise.parallel(queue)
    for DispatchQueue-based work such as AVFoundation session operations.
  • Use
    Promise.async
    only when wrapping Swift
    async
    /
    await
    or Task-native APIs end to end.
  • Avoid manually creating or passing around
    Promise<T>
    instances. Prefer
    Promise.parallel
    ,
    Promise.async
    ,
    Promise.resolved
    , and
    Promise.rejected
    ; use a manual Promise only for real native completion/delegate/callback bridges and keep it in the smallest scope with exactly-once completion.
  • Do not mix
    Promise.async
    with queue
    .sync
    calls or actor escape hatches.
  • 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
    /
    await
    或Task原生API时,才使用
    Promise.async
    。
  • 避免手动创建或传递
    Promise<T>
    实例。优先使用
    Promise.parallel
    、
    Promise.async
    、
    Promise.resolved
    和
    Promise.rejected
    ;仅在对接真实的原生完成/委托/回调时才使用手动Promise,并将其限制在最小作用域内,确保仅完成一次。
  • 不要将
    Promise.async
    与队列的
    .sync
    调用或actor逃逸机制混合使用。
  • 生成的HybridObject属性是同步入口点。如果它们需要队列亲和性,应将其重新设计为方法或监听器。