为什么需要关心严格并发检查?
Swift 5.5 引入了 async/await 和 Actor,但那时检查是"选择加入"的——编译器不会强制你遵守线程安全规则。到了 Swift 6,严格并发检查(Strict Concurrency Checking)默认全开。这意味着过去能编译的代码,现在可能直接报红。
对 SwiftUI 开发者来说,最典型的场景就是在视图中加载异步数据。你会遇到类似这样的错误:
1
2
| Main actor-isolated property 'users' cannot be passed
to non-isolated async function
|
别慌。这篇文章会带你从零搭建一套既安全又优雅的 SwiftUI 异步数据加载方案,并解释背后的原理。
基础版:Task + @MainActor
先从最直觉的方式开始。假设我们要从 API 加载用户列表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| @MainActor
final class UserViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var error: Error?
func loadUsers() async {
isLoading = true
defer { isLoading = false }
do {
let data = try await fetchUsersFromAPI()
users = data
} catch {
self.error = error
}
}
}
|
关键点:@MainActor 修饰的类,所有属性和方法都在主线程执行。await 暂停时让出主线程,恢复时自动回到主线程——这是 Swift 6 下最安全的做法。
进阶版:结构化并发
基础版有一个问题:如果用户快速切换页面,旧请求可能和新请求打架。加一个 Task 引用管理生命周期:
1
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
27
28
29
30
31
32
33
34
| @MainActor
final class UserViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var error: Error?
private var loadTask: Task<Void, Never>?
func loadUsers() {
// 取消上一次请求
loadTask?.cancel()
loadTask = Task {
isLoading = true
defer { isLoading = false }
do {
let data = try await fetchUsersFromAPI()
// 检查任务是否已被取消
try Task.checkCancellation()
users = data
} catch is CancellationError {
// 被取消时不视为错误
print("请求被取消")
} catch {
self.error = error
}
}
}
deinit {
loadTask?.cancel()
}
}
|
这段代码做了什么:
- 用
Task 引用管理异步操作,新请求自动取消旧的 Task.checkCancellation() 在每次 await 后检查是否被取消deinit 时自动取消,避免 ViewModel 销毁后回调还在执行
高阶版:分页加载
真实项目少不了分页。分页需要处理追加而非替换数据:
1
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
27
28
29
30
31
32
| @MainActor
final class PaginatedListViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoadingPage = false
@Published var hasMore = true
private var currentCursor: String?
func loadNextPage() async {
guard !isLoadingPage, hasMore else { return }
isLoadingPage = true
defer { isLoadingPage = false }
do {
let response: PaginatedResponse<Item> = try await API
.fetchItems(cursor: currentCursor)
items.append(contentsOf: response.items)
hasMore = response.hasMore
currentCursor = response.cursor
} catch {
print("分页加载失败")
}
}
func refresh() async {
currentCursor = nil
items = []
hasMore = true
await loadNextPage()
}
}
|
View 端如何使用
视图层的写法非常简洁直观:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| struct UserListView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
Group {
if viewModel.isLoading {
ProgressView("加载中...")
} else if let error = viewModel.error {
VStack {
Text("加载失败: " + error.localizedDescription)
Button("重试") {
Task { await viewModel.loadUsers() }
}
}
} else {
List(viewModel.users) { user in
Text(user.name)
}
.refreshable { await viewModel.loadUsers() }
}
}
.task { await viewModel.loadUsers() }
}
}
|
为什么不用 .onAppear 调用异步? .task 是 SwiftUI 专门为异步操作设计的修饰器——它会在视图消失时自动取消任务,不会有悬垂回调问题。而 .onAppear 触发的 fire-and-forget Task 在视图消失后依然在跑。
Swift 6 下常见的编译错误
| 错误信息 | 原因 | 解决方案 |
|---|
Main actor-isolated property ... | 从非隔离上下文修改 @Published | 给 ViewModel 加 @MainActor |
Capture of self with non-sendable type | 闭包捕获非 Sendable 的 self | 声明 @unchecked Sendable 或重构 |
Reference to var is not concurrency-safe | 跨 actor 共享可变状态 | 用 Actor 封装或加 @MainActor |
实用模式:MainActor.run 桥接
有时无法给整个类加 @MainActor(比如它需要遵循某个非 MainActor 协议),这时可以手动切到主线程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class MixedViewModel: SomeNonIsolatedProtocol {
var results: [String] = []
func updateResults(_ newData: [String]) async {
let processed = await processData(newData)
await MainActor.run {
self.results = processed
}
}
nonisolated func processData(_ data: [String]) async -> [String] {
data.map { $0.uppercased() }
}
}
|
总结
Swift 6 的严格并发检查是帮你在编译期就发现线程安全问题。对于 SwiftUI 开发,遵循三条原则就能避开 90% 的问题:
- ViewModel 加
@MainActor,让 SwiftUI 相关操作自动在主线程执行 - 使用
.task 而非 .onAppear + Task,让 SwiftUI 管理生命周期 - 每个 View 只用少量 ViewModel,通过
@Observable 或 ObservableObject 绑定数据
以上代码在 Swift 6 + iOS 18 环境下测试通过。如果你的项目还在迁移中,可以在 Build Settings 中将 Strict Concurrency Checking 设为 Minimal 逐步过渡,但建议尽快全开——越早适应,踩坑越少。