swift-conventions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Swift Conventions — Expert Decisions

Swift 编码规范——专业决策指南

Expert decision frameworks for Swift choices that require experience. Claude knows Swift syntax — this skill provides the judgment calls.

针对需要经验判断的Swift选型提供专业决策框架。Claude了解Swift语法——本技能提供关键的判断依据。

Decision Trees

决策树

Struct vs Class

Struct 与 Class 选型

Need shared mutable state across app?
├─ YES → Class (singleton pattern, session managers)
└─ NO
   └─ Need inheritance hierarchy?
      ├─ YES → Class (UIKit subclasses, NSObject interop)
      └─ NO
         └─ Data model or value type?
            ├─ YES → Struct (User, Configuration, Point)
            └─ NO → Consider what identity means
               ├─ Same instance matters → Class
               └─ Same values matters → Struct
The non-obvious trade-off: Structs with reference-type properties (arrays, classes inside) lose copy-on-write benefits. A
struct
containing
[UIImage]
copies the array reference, not images — mutations affect all "copies."
是否需要在应用中共享可变状态?
├─ 是 → Class(单例模式、会话管理器)
└─ 否
   └─ 是否需要继承层级?
      ├─ 是 → Class(UIKit子类、NSObject互操作)
      └─ 否
         └─ 是否为数据模型或值类型?
            ├─ 是 → Struct(User、Configuration、Point)
            └─ 否 → 考虑标识的含义
               ├─ 实例一致性重要 → Class
               └─ 值一致性重要 → Struct
容易忽略的权衡点:包含引用类型属性(数组、内部Class)的Struct会失去写时复制(COW)的优势。例如包含
[UIImage]
struct
只会复制数组引用,而非图片本身——修改操作会影响所有“副本”。

async/await vs Combine vs Callbacks

async/await、Combine 与回调的选型

Is this a one-shot operation? (fetch user, save file)
├─ YES → async/await (cleaner, better stack traces)
└─ NO → Is it a stream of values over time?
   ├─ YES
   │  └─ Need transformations/combining?
   │     ├─ Heavy transforms → Combine (map, filter, merge)
   │     └─ Simple iteration → AsyncStream
   └─ NO → Must support iOS 14?
      ├─ YES → Combine or callbacks
      └─ NO → async/await with continuation
When Combine still wins: Multiple publishers needing
combineLatest
,
merge
, or
debounce
. Converting this to pure async/await requires manual coordination that Combine handles elegantly.
是否为一次性操作?(获取用户、保存文件)
├─ 是 → async/await(代码更简洁、栈追踪更清晰)
└─ 否 → 是否为持续的数据流?
   ├─ 是
   │  └─ 是否需要转换/合并操作?
   │     ├─ 复杂转换 → Combine(map、filter、merge)
   │     └─ 简单迭代 → AsyncStream
   └─ 否 → 是否需要支持iOS 14?
      ├─ 是 → Combine 或回调
      └─ 否 → 结合continuation使用async/await
Combine仍占优的场景:需要使用
combineLatest
merge
debounce
的多发布者场景。将此类场景转换为纯async/await需要手动协调,而Combine可以优雅处理。

@MainActor Placement

@MainActor 放置规则

Is every public method UI-related?
├─ YES → @MainActor on class/struct
└─ NO
   └─ Does it manage UI state? (@Published, bindings)
      ├─ YES → @MainActor on class, nonisolated for non-UI methods
      └─ NO
         └─ Only some methods touch UI?
            ├─ YES → @MainActor on specific methods
            └─ NO → No @MainActor needed
Critical:
@Published
properties MUST be updated on MainActor. SwiftUI observes on main thread — background updates cause undefined behavior, not just warnings.
所有公开方法是否都与UI相关?
├─ 是 → 在class/struct上添加@MainActor
└─ 否
   └─ 是否管理UI状态?(@Published、绑定)
      ├─ 是 → 在class上添加@MainActor,非UI方法标记nonisolated
      └─ 否
         └─ 是否只有部分方法涉及UI?
            ├─ 是 → 在特定方法上添加@MainActor
            └─ 否 → 无需添加@MainActor
关键注意事项
@Published
属性必须在MainActor上更新。SwiftUI在主线程监听状态——后台更新会导致未定义行为,而非仅仅是警告。

TaskGroup vs async let

TaskGroup 与 async let 选型

Number of concurrent operations known at compile time?
├─ YES (2-5 fixed operations) → async let
│  Example: async let user = fetchUser()
│           async let posts = fetchPosts()
└─ NO (dynamic count, array of IDs) → TaskGroup
   Example: for id in userIds { group.addTask { ... } }
async let gotcha: All
async let
values MUST be awaited before scope ends. Forgetting to await silently cancels the task — no error, just missing data.

并发操作的数量在编译时是否已知?
├─ 是(2-5个固定操作)→ async let
│  示例:async let user = fetchUser()
│           async let posts = fetchPosts()
└─ 否(动态数量、ID数组)→ TaskGroup
   示例:for id in userIds { group.addTask { ... } }
async let 的陷阱:所有
async let
的值必须在作用域结束前被await。忘记await会静默取消任务——不会报错,但会丢失数据。

NEVER Do

绝对禁止的操作

Memory & Retain Cycles

内存与循环引用

NEVER capture
self
strongly in stored closures:
swift
// ❌ Retain cycle — ViewModel never deallocates
class ViewModel {
    var onUpdate: (() -> Void)?

    func setup() {
        onUpdate = { self.refresh() } // self → onUpdate → self
    }
}

// ✅ Break with weak capture
onUpdate = { [weak self] in self?.refresh() }
NEVER use
unowned
unless you can PROVE the reference outlives the closure. When in doubt, use
weak
. The crash from dangling
unowned
is worse than the nil-check cost.
NEVER forget Timer invalidation:
swift
// ❌ Timer retains target — object never deallocates
timer = Timer.scheduledTimer(target: self, selector: #selector(tick), ...)

// ✅ Block-based with weak capture + invalidate in deinit
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
    self?.tick()
}
deinit { timer?.invalidate() }
绝对禁止在存储闭包中强引用
self
swift
// ❌ 循环引用——ViewModel永远不会被释放
class ViewModel {
    var onUpdate: (() -> Void)?

    func setup() {
        onUpdate = { self.refresh() } // self → onUpdate → self
    }
}

// ✅ 使用弱引用打破循环
onUpdate = { [weak self] in self?.refresh() }
绝对禁止使用
unowned
,除非你能证明引用的生命周期长于闭包。不确定时,请使用
weak
unowned
引用失效导致的崩溃比空检查的代价严重得多。
绝对禁止忘记销毁Timer:
swift
// ❌ Timer持有目标对象——对象永远不会被释放
timer = Timer.scheduledTimer(target: self, selector: #selector(tick), ...)

// ✅ 基于闭包的实现+弱引用+在deinit中销毁
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
    self?.tick()
}
deinit { timer?.invalidate() }

Concurrency

并发处理

NEVER access
@Published
from background:
swift
// ❌ Undefined behavior — may work sometimes, crash others
Task.detached {
    viewModel.isLoading = false // Background thread!
}

// ✅ Explicit MainActor
Task { @MainActor in
    viewModel.isLoading = false
}
NEVER use
Task { }
for fire-and-forget without understanding cancellation:
swift
// ❌ Task inherits actor context — may block UI
func buttonTapped() {
    Task { await heavyOperation() } // Runs on MainActor!
}

// ✅ Explicit detachment for background work
func buttonTapped() {
    Task.detached(priority: .userInitiated) {
        await heavyOperation()
    }
}
NEVER assume
Task.cancel()
stops execution immediately. Cancellation is cooperative — your code must check
Task.isCancelled
or use
try Task.checkCancellation()
.
绝对禁止从后台线程访问
@Published
属性:
swift
// ❌ 未定义行为——有时能运行,有时会崩溃
Task.detached {
    viewModel.isLoading = false // 后台线程!
}

// ✅ 显式指定MainActor
Task { @MainActor in
    viewModel.isLoading = false
}
绝对禁止在不理解取消机制的情况下,使用
Task { }
执行“即发即弃”的任务:
swift
// ❌ Task继承actor上下文——可能阻塞UI
func buttonTapped() {
    Task { await heavyOperation() } // 在MainActor上运行!
}

// ✅ 显式分离到后台执行耗时操作
func buttonTapped() {
    Task.detached(priority: .userInitiated) {
        await heavyOperation()
    }
}
绝对禁止假设
Task.cancel()
会立即停止执行。取消是协作式的——你的代码必须检查
Task.isCancelled
或使用
try Task.checkCancellation()

Optionals

可选类型

NEVER force-unwrap in production code except:
  1. @IBOutlet
    — set by Interface Builder
  2. URL(string: "https://known-valid.com")!
    — compile-time known strings
  3. fatalError
    paths where crash is correct behavior
NEVER use implicitly unwrapped optionals (
var user: User!
) for regular properties. Only valid for:
  • @IBOutlet
    connections
  • Two-phase initialization where value is set immediately after init
绝对禁止在生产代码中强制解包,除非满足以下场景:
  1. @IBOutlet
    ——由Interface Builder设置
  2. URL(string: "https://known-valid.com")!
    ——编译时已知的有效字符串
  3. fatalError
    路径——崩溃是预期行为
绝对禁止为常规属性使用隐式解包可选类型(
var user: User!
)。仅在以下场景有效:
  • @IBOutlet
    连接
  • 分阶段初始化——值在init后立即设置

Protocol Design

协议设计

NEVER make protocols require
AnyObject
unless you need
weak
references:
swift
// ❌ Unnecessarily restricts to classes
protocol DataProvider: AnyObject {
    func fetchData() -> Data
}

// ✅ Only require AnyObject for delegates that need weak reference
protocol ViewModelDelegate: AnyObject { // Needed for weak var delegate
    func viewModelDidUpdate()
}
NEVER add default implementations that change protocol semantics:
swift
// ❌ Dangerous — conformers might not override
protocol Validator {
    func validate() -> Bool
}
extension Validator {
    func validate() -> Bool { true } // Silent "always valid"
}

// ✅ Make requirement obvious or use different name
extension Validator {
    func isAlwaysValid() -> Bool { true } // Clear this is a default
}

绝对禁止在不需要
weak
引用的情况下,要求协议继承
AnyObject
swift
// ❌ 不必要地限制为Class类型
protocol DataProvider: AnyObject {
    func fetchData() -> Data
}

// ✅ 仅在需要weak引用的委托协议中要求AnyObject
protocol ViewModelDelegate: AnyObject { // 为weak var delegate所必需
    func viewModelDidUpdate()
}
绝对禁止添加会改变协议语义的默认实现:
swift
// ❌ 危险——遵循者可能不会重写
protocol Validator {
    func validate() -> Bool
}
extension Validator {
    func validate() -> Bool { true } // 静默的“始终有效”
}

// ✅ 明确要求实现或使用不同名称
extension Validator {
    func isAlwaysValid() -> Bool { true } // 明确这是默认实现
}

iOS-Specific Patterns

iOS特有的设计模式

Dependency Injection in ViewModels

ViewModel中的依赖注入

swift
// ✅ Protocol-based for testability
protocol UserServiceProtocol {
    func fetchUser(id: String) async throws -> User
}

@MainActor
final class UserViewModel: ObservableObject {
    @Published private(set) var user: User?
    @Published private(set) var error: Error?

    private let userService: UserServiceProtocol

    init(userService: UserServiceProtocol = UserService()) {
        self.userService = userService
    }
}
Why default parameter: Production code uses real service, tests inject mock. No container framework needed for most apps.
swift
// ✅ 基于协议的实现,便于测试
protocol UserServiceProtocol {
    func fetchUser(id: String) async throws -> User
}

@MainActor
final class UserViewModel: ObservableObject {
    @Published private(set) var user: User?
    @Published private(set) var error: Error?

    private let userService: UserServiceProtocol

    init(userService: UserServiceProtocol = UserService()) {
        self.userService = userService
    }
}
默认参数的作用:生产代码使用真实服务,测试时注入Mock。大多数应用不需要依赖注入容器框架。

Property Wrapper Selection

属性包装器选型

WrapperUse WhenMemory Behavior
@State
View-local primitive/value typesView-owned, recreated on parent rebuild
@StateObject
View creates and owns the ObservableObjectCreated once, survives view rebuilds
@ObservedObject
View receives ObservableObject from parentNot owned, may be recreated
@EnvironmentObject
Shared across view hierarchyMust be injected by ancestor
@Binding
Two-way connection to parent's stateReference to parent's storage
The StateObject vs ObservedObject trap: Using
@ObservedObject
for a locally-created object causes recreation on every view update — losing all state.
包装器使用场景内存行为
@State
视图本地的基本类型/值类型视图拥有,父视图重建时会重新创建
@StateObject
视图创建并拥有的ObservableObject仅创建一次,在视图重建时保留
@ObservedObject
视图从父视图接收的ObservableObject不被视图拥有,可能被重新创建
@EnvironmentObject
在视图层级中共享必须由祖先视图注入
@Binding
与父视图状态的双向绑定引用父视图的存储
StateObject vs ObservedObject 的陷阱:为本地创建的对象使用
@ObservedObject
会导致每次视图更新时对象被重新创建——丢失所有状态。

Error Handling Strategy

错误处理策略

swift
// Domain-specific errors with recovery info
enum UserError: LocalizedError {
    case notFound(userId: String)
    case unauthorized
    case networkFailure(underlying: Error)

    var errorDescription: String? {
        switch self {
        case .notFound(let id): return "User \(id) not found"
        case .unauthorized: return "Please log in again"
        case .networkFailure: return "Connection failed"
        }
    }

    var recoverySuggestion: String? {
        switch self {
        case .notFound: return "Check the user ID and try again"
        case .unauthorized: return "Your session expired"
        case .networkFailure: return "Check your internet connection"
        }
    }
}

swift
// 包含恢复信息的领域特定错误
enum UserError: LocalizedError {
    case notFound(userId: String)
    case unauthorized
    case networkFailure(underlying: Error)

    var errorDescription: String? {
        switch self {
        case .notFound(let id): return "用户 \(id) 不存在"
        case .unauthorized: return "请重新登录"
        case .networkFailure: return "连接失败"
        }
    }

    var recoverySuggestion: String? {
        switch self {
        case .notFound: return "检查用户ID后重试"
        case .unauthorized: return "你的会话已过期"
        case .networkFailure: return "检查你的网络连接"
        }
    }
}

Performance Traps

性能陷阱

Copy-on-Write Gotchas

写时复制(COW)陷阱

swift
// ✅ COW works — array copied only on mutation
var a = [1, 2, 3]
var b = a        // No copy yet
b.append(4)      // Now b gets its own copy

// ❌ COW broken — class inside struct
struct Container {
    var items: NSMutableArray // Reference type!
}
var c1 = Container(items: NSMutableArray())
var c2 = c1      // Both point to same NSMutableArray
c2.items.add(1)  // Mutates c1.items too!
swift
// ✅ COW正常工作——仅在修改时复制数组
var a = [1, 2, 3]
var b = a        // 尚未复制
b.append(4)      // 此时b会创建自己的副本

// ❌ COW失效——Struct内部包含Class
struct Container {
    var items: NSMutableArray // 引用类型!
}
var c1 = Container(items: NSMutableArray())
var c2 = c1      // 两者指向同一个NSMutableArray
c2.items.add(1)  // 同时修改了c1.items!

Lazy vs Computed

延迟属性 vs 计算属性

swift
// lazy: Computed ONCE, stored
lazy var dateFormatter: DateFormatter = {
    let f = DateFormatter()
    f.dateStyle = .medium
    return f
}()

// computed: Computed EVERY access
var formattedDate: String {
    dateFormatter.string(from: date) // Cheap, uses cached formatter
}
Rule: Expensive object creation →
lazy
. Simple derived values → computed.
swift
// lazy:仅计算一次,存储结果
lazy var dateFormatter: DateFormatter = {
    let f = DateFormatter()
    f.dateStyle = .medium
    return f
}()

// computed:每次访问都会重新计算
var formattedDate: String {
    dateFormatter.string(from: date) // 开销低,使用缓存的formatter
}
规则:昂贵的对象创建 →
lazy
。简单的派生值 → 计算属性。

String Performance

字符串性能优化

swift
// ❌ O(n) for each concatenation in loop
var result = ""
for item in items {
    result += item.description // Creates new String each time
}

// ✅ O(n) total
var result = ""
result.reserveCapacity(estimatedLength)
for item in items {
    result.append(item.description)
}

// ✅ Best for joining
let result = items.map(\.description).joined(separator: ", ")

swift
// ❌ 循环中每次拼接都是O(n)时间复杂度
var result = ""
for item in items {
    result += item.description // 每次都会创建新的String
}

// ✅ 总时间复杂度O(n)
var result = ""
result.reserveCapacity(estimatedLength)
for item in items {
    result.append(item.description)
}

// ✅ 最佳拼接方式
let result = items.map(\.description).joined(separator: ", ")

Quick Reference

快速参考

Access Control Decision

访问控制决策

LevelUse When
private
Implementation detail within declaration
fileprivate
Shared between types in same file (rare)
internal
Module-internal, app code default
package
Same package, different module (Swift 5.9+)
public
Framework API, readable outside module
open
Framework API, subclassable outside module
Default to most restrictive. Start
private
, widen only when needed.
级别使用场景
private
声明内部的实现细节
fileprivate
同一文件内的类型共享(罕见)
internal
模块内部,应用代码默认级别
package
同一Package,不同模块(Swift 5.9+)
public
框架API,可被模块外部访问
open
框架API,可被模块外部继承
原则:默认使用最严格的访问级别。从
private
开始,仅在需要时放宽。

Naming Quick Check

命名快速检查

  • Types:
    PascalCase
    nouns —
    UserViewModel
    ,
    NetworkError
  • Protocols:
    PascalCase
    — capability (
    -able/-ible
    ) or description
  • Functions:
    camelCase
    verbs —
    fetchUser()
    ,
    configure(with:)
  • Booleans:
    is/has/should/can
    prefix —
    isLoading
    ,
    hasContent
  • Factory methods:
    make
    prefix —
    makeUserViewModel()
  • 类型:
    PascalCase
    名词 —
    UserViewModel
    ,
    NetworkError
  • 协议:
    PascalCase
    — 表示能力(后缀
    -able/-ible
    )或描述性命名
  • 函数:
    camelCase
    动词 —
    fetchUser()
    ,
    configure(with:)
  • 布尔值:前缀
    is/has/should/can
    isLoading
    ,
    hasContent
  • 工厂方法:前缀
    make
    makeUserViewModel()