Swift 语法与最佳实践
# Swift 语法与最佳实践
本文档整合 Swift 语法基础与开发最佳实践,供日常查阅与团队参考。 最后更新:2026-09-21
# 语法基础
# 参数标签
// 在这个函数中, than 是参数标签, 它用于在函数调用时提供更多的上下文和可读性。number 是参数名称, 它是函数内部用来引用传递给函数的整数值的名称。Int 表示参数的类型, 即函数接受一个整数作为参数
func oneMore(than number: Int) -> Int {
return number + 1
}
var myNumber = 1
// than 是参数标签, myNumber 是传递给函数的参数名称。这有助于使代码更易读懂, 尤其是在函数有多个参数时
myNumber = oneMore(than: myNumber)
print(myNumber)
2
3
4
5
6
7
8
9
10
11
如果您愿意, 也可以省略参数标签, 函数调用会变得更加简洁, 但通常会失去一些可读性
# in-out 关键字
in-out 参数是 Swift 中的一种参数类型, 它允许函数修改调用者传递给该函数的参数的值。通常, 函数内的参数是常量, 这意味着您不能在函数内修改它们的值。但是, 如果将参数声明为 in-out, 函数可以修改该参数的值, 并且这些更改将在函数调用后保持有效
要声明 in-out 参数, 您需要在参数类型前面加上 inout 关键字。这是一个示例:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var x = 1
var y = 2
swapTwoInts(&x, &y)
print("x is now \(x), and y is now \(y)")
2
3
4
5
6
7
8
9
10
11
12
在这个示例中, swapTwoInts 函数接受两个 in-out 整数参数 a 和 b。通过使用 & 符号, 我们将 x 和 y 变量传递给函数, 使函数能够修改它们的值。函数内部实现了一个经典的交换两个整数值的算法
需要注意的是, in-out 参数必须具有可变的存储属性, 例如变量, 而不是常量。此外, in-out 参数不能有默认值, 它们通常不能是可选类型, 并且它们不能用于不逃逸的闭包
使用 in-out 参数时要小心, 因为它们会更改原始值, 可能会导致复杂的代码行为。通常情况下, 最好避免使用 in-out 参数, 而是使用返回值来传递修改后的值
# & 符号的常见用途
在 Swift 中, & 符号用于多种不同的上下文, 它的含义取决于它的使用方式。以下是 & 符号的一些常见用途:
- 引用参数(In-Out Parameters): 在函数或方法中, 你可以将参数标记为 in-out, 以允许函数修改传递给它的参数。这时, 你需要在参数前面加上 & 符号, 表示你要传递参数的引用。例如:
func increment(_ number: inout Int) {
number += 1
}
var myNumber = 5
increment(&myNumber) // 这里需要使用 & 符号传递参数的引用
2
3
4
5
6
- 取地址(Address Of): 在某些情况下, 你可能需要获取一个变量或常量的内存地址。你可以使用 & 符号来获得它。例如:
var someValue = 42
let pointerToValue = &someValue // 获取 someValue 的内存地址
2
- 位操作(Bitwise Operations): 在位操作中, & 表示按位与运算, 用于比较两个二进制值的每一位, 只有当两者都为 1 时结果才为 1。例如:
let a: UInt8 = 0b00101010
let b: UInt8 = 0b10110000
let result = a & b // result 的值为 0b00100000
2
3
- 逻辑与(Logical AND): & 也可以用作逻辑与运算符, 用于组合布尔表达式中的条件。例如:
if condition1 && condition2 {
// 如果 condition1 和 condition2 都为 true, 则执行此块
}
2
3
这些是 & 符号在 Swift 中的一些常见用途, 其含义会根据上下文的不同而变化
# 一、闭包捕获与循环引用
# 1.1 闭包默认强引用
Swift 闭包默认会强引用捕获的对象。如果闭包被对象持有,同时闭包又强引用该对象,就会形成循环引用(Reference Cycle),导致内存泄漏。
// ❌ 循环引用示例
class MyClass {
var closure: (() -> Void)?
func setup() {
closure = {
self.doSomething() // 闭包强引用 self
}
}
// MyClass 强引用 closure,closure 强引用 MyClass → 循环引用
}
2
3
4
5
6
7
8
9
10
11
# 1.2 捕获列表:[weak self] vs [unowned self]
| 特性 | [weak self] | [unowned self] |
|---|---|---|
| 引用类型 | 弱引用 | 无主引用 |
| 是否可选 | 是(self?) | 否(self,直接访问) |
| self 被释放后 | 安全(nil,不执行) | 💥 崩溃(悬垂指针) |
| 适用场景 | self 可能被释放 | self 一定不会被释放,且闭包生命周期不超过 self |
# [weak self] 示例
// ✅ 使用 [weak self] 避免循环引用
DispatchQueue.main.async { [weak self] in
self?.objectWillChange.send() // self? 为 nil 时安全跳过
}
2
3
4
# [unowned self] 示例
// ✅ 确定 self 不会被释放时使用 [unowned self]
UIView.animate(withDuration: 0.3) { [unowned self] in
self.view.layoutIfNeeded() // self 一定存在,直接访问
}
2
3
4
# 1.3 什么时候必须用 [weak self]?
| 场景 | 是否需要 [weak self] | 原因 |
|---|---|---|
DispatchQueue.main.async | ✅ 推荐 | 闭包被 GCD 持有,可能形成循环引用 |
Timer.scheduledTimer | ✅ 必须 | Timer 强引用 target,容易形成循环引用 |
UIView.animate | ⚠️ 可选 | 闭包执行完毕后释放,通常不会泄漏,但用 [weak self] 更安全 |
即时执行的闭包(如 map、filter) | ❌ 不需要 | 闭包即时执行,不会被持有 |
@escaping 闭包 | ✅ 推荐 | 逃逸闭包可能被持有,容易形成循环引用 |
| 单例对象的闭包 | ⚠️ 可选 | 单例不会被释放,但用 [weak self] 是最佳实践 |
# 1.4 循环引用检测工具
- Xcode Memory Graph Debugger:运行时查看对象引用关系
- Instruments - Leaks:检测内存泄漏
- deinit 打印日志:在
deinit中打印日志,确认对象是否被释放
deinit {
print("\(type(of: self)) deinit") // 如果没打印,说明可能有循环引用
}
2
3
# 二、GCD 与异步编程
# 2.1 常用队列
| 队列 | 说明 | 使用场景 |
|---|---|---|
DispatchQueue.main | 主线程串行队列 | UI 更新、用户交互 |
DispatchQueue.global() | 全局并发队列 | 耗时操作(网络、计算、IO) |
DispatchQueue(label:) | 自定义串行队列 | 保护共享资源、顺序执行任务 |
DispatchQueue(label:, attributes: .concurrent) | 自定义并发队列 | 并发执行任务 |
# 2.2 异步派发
// 主线程异步执行(下一个 runloop)
DispatchQueue.main.async {
// UI 更新
}
// 后台线程异步执行
DispatchQueue.global().async {
// 耗时操作
DispatchQueue.main.async {
// 回到主线程更新 UI
}
}
// 延迟执行
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
// 1 秒后执行
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.3 async 为什么能解决视图更新警告?
问题:在视图更新过程中调用 objectWillChange.send() 会触发警告:
Publishing changes from within view updates is not allowed
原因:SwiftUI 的视图更新在当前 runloop 中同步执行,如果此时发布变化,会导致未定义行为。
解决方案:用 DispatchQueue.main.async 将发布变化推迟到下一个 runloop:
DispatchQueue.main.async { [weak self] in
self?.objectWillChange.send() // 下一个 runloop 执行,当前视图更新已完成
}
2
3
关键点:不是"判定"是否在视图更新过程中,而是"确保"当前事务完成后再执行。
# 三、SwiftUI 属性包装器
# 3.1 常用属性包装器对比
| 属性包装器 | 用途 | 存储位置 | 触发视图更新 |
|---|---|---|---|
@State | 视图内部私有状态 | 视图内存 | ✅ |
@Binding | 引用父视图的 @State | 不存储,引用 | ✅ |
@ObservedObject | 观察外部对象 | 外部对象 | ✅(对象的 objectWillChange) |
@StateObject | 视图拥有的可观察对象 | 视图内存 | ✅ |
@EnvironmentObject | 全局共享对象 | 环境 | ✅ |
@Environment | 系统环境值(如 colorScheme) | 系统环境 | ✅ |
@AppStorage | UserDefaults 持久化 | UserDefaults | ✅(在视图中) |
@Published | 可观察对象的属性 | 对象内存 | ✅(触发 objectWillChange) |
# 3.2 @StateObject vs @ObservedObject
| 特性 | @StateObject | @ObservedObject |
|---|---|---|
| 所有权 | 视图拥有 | 外部拥有,视图观察 |
| 生命周期 | 与视图相同 | 不拥有,可能被释放 |
| 适用场景 | 视图创建并拥有的对象 | 外部传入的对象 |
| 注意事项 | 只会初始化一次 | 每次视图重建都可能重新初始化 |
// ✅ 视图拥有的对象用 @StateObject
struct MyView: View {
@StateObject var viewModel = ViewModel() // 视图拥有,生命周期与视图相同
}
// ✅ 外部传入的对象用 @ObservedObject
struct MyView: View {
@ObservedObject var viewModel: ViewModel // 外部拥有,视图只观察
}
2
3
4
5
6
7
8
9
# 3.3 @AppStorage 在 ObservableObject 中的坑
问题:@AppStorage 实现了 DynamicProperty 协议,它的 update() 方法只有在 SwiftUI 视图中才会被自动调用。在 ObservableObject 中,update() 不会被自动调用,所以 @AppStorage 的缓存值可能不会更新。
现象:在设置页面修改了 @AppStorage 属性,但在 ObservableObject 中读取到的还是旧值。
解决方案:在 ObservableObject 的 init 中监听 UserDefaults.didChangeNotification,主动调用所有 @AppStorage 属性的 update() 方法:
final class AppSettings: ObservableObject {
static let shared = AppSettings()
@AppStorage("showOverlayOnBackground") var showOverlayOnBackground: Bool = true
// ... 其他 @AppStorage 属性
private init() {
NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.refreshFromUserDefaults()
}
}
private func refreshFromUserDefaults() {
_showOverlayOnBackground.update()
// ... 其他属性的 update()
// 异步派发,避免在视图更新过程中调用
DispatchQueue.main.async { [weak self] in
self?.objectWillChange.send()
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 四、SwiftUI 视图更新机制
# 4.1 视图更新触发条件
SwiftUI 视图在以下情况会重新计算 body:
@State属性变化@ObservedObject/@StateObject/@EnvironmentObject的objectWillChange触发@Environment值变化(如colorScheme)- 父视图重新渲染(子视图可能重新渲染)
@AppStorage值变化(在视图中)
# 4.2 视图更新过程
当前 runloop
├── 状态变化(@State / objectWillChange)
├── SwiftUI 标记视图为"需要更新"
├── runloop 结束时,批量执行视图更新
│ ├── 计算 body
│ ├── diff(比较新旧视图)
│ └── 渲染
└── runloop 结束
2
3
4
5
6
7
8
# 4.3 为什么不能在视图更新过程中发布变化?
如果在视图更新过程中(比如 body 计算、onAppear)调用 objectWillChange.send():
- SwiftUI 正在更新视图,此时又收到"对象将要变化"的通知
- 可能导致无限循环、重复更新、或未定义行为
- SwiftUI 会输出警告:
Publishing changes from within view updates is not allowed
解决方案:用 DispatchQueue.main.async 推迟到下一个 runloop。
# 4.4 onAppear vs onChange vs task
| 修饰符 | 触发时机 | 适用场景 |
|---|---|---|
.onAppear | 视图出现时 | 初始化、加载数据 |
.onDisappear | 视图消失时 | 清理、取消任务 |
.onChange(of:) | 某个值变化时 | 响应状态变化 |
.task | 视图出现时,异步任务 | 异步加载数据(自动取消) |
# 五、内存管理最佳实践
# 5.1 单例的使用
// ✅ 推荐:线程安全的单例
final class MyManager {
static let shared = MyManager() // let 是线程安全的
private init() {} // 禁止外部创建
}
// ❌ 不推荐:非线程安全的单例
final class MyManager {
static var shared: MyManager!
private init() {}
}
2
3
4
5
6
7
8
9
10
11
# 5.2 避免强引用循环
| 场景 | 解决方案 |
|---|---|
| 闭包捕获 self | [weak self] 或 [unowned self] |
| delegate 模式 | delegate 用 weak |
| Timer | 用 [weak self],或在 deinit 中 invalidate() |
| 通知中心 | 在 deinit 中移除观察者,或使用 [weak self] |
| 异步任务 | 用 [weak self],任务完成后检查 self 是否存在 |
# 5.3 deinit 调试技巧
deinit {
print("✅ \(type(of: self)) deinit") // 确认对象被释放
}
2
3
如果 deinit 没有被调用,说明可能有循环引用。
# 六、错误处理最佳实践
# 6.1 try? vs try! vs do-catch
| 方式 | 说明 | 适用场景 |
|---|---|---|
try? | 错误返回 nil | 错误不重要,可以忽略 |
try! | 错误直接崩溃 | 确定不会出错(如资源文件加载) |
do-catch | 捕获并处理错误 | 需要处理错误的场景 |
// ✅ 推荐:do-catch 处理错误
do {
let data = try Data(contentsOf: url)
} catch {
print("加载失败: \(error)")
}
// ✅ 可以忽略错误时用 try?
let data = try? Data(contentsOf: url)
// ❌ 避免:除非确定不会出错
let data = try! Data(contentsOf: url) // 出错会崩溃
2
3
4
5
6
7
8
9
10
11
12
# 6.2 自定义错误类型
enum AppError: Error, LocalizedError {
case invalidPassword
case decryptionFailed
case fileNotFound(String)
var errorDescription: String? {
switch self {
case .invalidPassword:
return "密码错误"
case .decryptionFailed:
return "解密失败"
case .fileNotFound(let path):
return "文件不存在: \(path)"
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 七、线程安全最佳实践
# 7.1 共享资源保护
// ✅ 推荐:用串行队列保护共享资源
final class ThreadSafeStore {
private let queue = DispatchQueue(label: "com.example.store")
private var _items: [Item] = []
var items: [Item] {
queue.sync { _items } // 读用 sync
}
func add(_ item: Item) {
queue.async { self._items.append(item) } // 写用 async
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 7.2 主线程更新 UI
// ✅ 确保在主线程更新 UI
DispatchQueue.main.async {
self.label.text = "新内容"
}
// ✅ 检查是否在主线程
func updateUI() {
if Thread.isMainThread {
label.text = "新内容"
} else {
DispatchQueue.main.async { self.label.text = "新内容" }
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 八、代码组织最佳实践
# 8.1 目录结构
CipherNest/
├── App/ # App 入口、配置
├── Views/ # SwiftUI 视图
│ ├── Components/ # 可复用组件
│ └── Modifiers/ # 自定义 ViewModifier
├── ViewModels/ # 视图模型
├── Models/ # 数据模型
├── Services/ # 业务逻辑服务
├── Extensions/ # 扩展
├── Utilities/ # 工具类
├── Debug/ # 调试相关(#if DEBUG)
└── Resources/ # 资源文件
2
3
4
5
6
7
8
9
10
11
12
# 8.2 日志工具
// ✅ 推荐:每个文件有自己的 log 实例,tag 为文件名
struct MyView: View {
private let log = LogHelper(tag: "MyView")
var body: some View {
Text("Hello")
.onAppear {
log.info("onAppear")
}
}
}
2
3
4
5
6
7
8
9
10
11
# 8.3 扩展文件命名
Extensions/
├── String+Extension.swift
├── Date+Extension.swift
├── Color+Extension.swift
└── View+Extension.swift
2
3
4
5
# 九、性能优化最佳实践
# 9.1 避免不必要的视图更新
// ✅ 使用 Equatable 避免不必要的更新
struct Item: Identifiable, Equatable {
let id: UUID
var title: String
}
// ✅ 使用 .id() 强制视图重建
MyView()
.id(item.id) // id 变化时强制重建
2
3
4
5
6
7
8
9
# 9.2 懒加载
// ✅ 大列表用 LazyVStack / LazyHStack
ScrollView {
LazyVStack {
ForEach(items) { item in
ItemRow(item: item)
}
}
}
2
3
4
5
6
7
8
# 9.3 避免在 body 中做耗时操作
// ❌ 不推荐:在 body 中做耗时操作
var body: some View {
let data = loadFromDisk() // 每次 body 计算都执行
Text(data)
}
// ✅ 推荐:在 onAppear 或 task 中加载
@State private var data: String = ""
var body: some View {
Text(data)
.task {
data = await loadFromDisk()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 十、调试技巧
# 10.1 打印调用栈
// 打印当前调用栈,用于排查问题
func debugCallStack() {
print("📞 Call stack:")
for symbol in Thread.callStackSymbols.prefix(10) {
print(" \(symbol)")
}
}
2
3
4
5
6
7
# 10.2 测量执行时间
func measure<T>(_ label: String, _ block: () -> T) -> T {
let start = CFAbsoluteTimeGetCurrent()
let result = block()
let elapsed = (CFAbsoluteTimeGetCurrent() - start) * 1000
print("⏱️ \(label): \(String(format: "%.1f", elapsed))ms")
return result
}
// 使用
measure("加载数据") {
loadData()
}
2
3
4
5
6
7
8
9
10
11
12
# 10.3 条件编译
#if DEBUG
// 调试代码,Release 不会打包
debugPrint("调试信息")
#endif
#if targetEnvironment(simulator)
// 模拟器专用代码
#endif
2
3
4
5
6
7
8