目录

SwiftUI 异步数据加载与 Swift 6 严格并发检查实战

为什么需要关心严格并发检查?

Swift 5.5 引入了 async/awaitActor,但那时检查是"选择加入"的——编译器不会强制你遵守线程安全规则。到了 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% 的问题:

  1. ViewModel 加 @MainActor,让 SwiftUI 相关操作自动在主线程执行
  2. 使用 .task 而非 .onAppear + Task,让 SwiftUI 管理生命周期
  3. 每个 View 只用少量 ViewModel,通过 @ObservableObservableObject 绑定数据

以上代码在 Swift 6 + iOS 18 环境下测试通过。如果你的项目还在迁移中,可以在 Build Settings 中将 Strict Concurrency Checking 设为 Minimal 逐步过渡,但建议尽快全开——越早适应,踩坑越少。