Swift Continuations 完全指南:一口气弄懂 4 种“桥梁”
一、为什么需要 Continuations?
Swift 5.5 带来 async/await,但:
- 老 SDK / 三方库仍用回调
- 自己封装的
DispatchQueue
、Timer
、NotificationCenter
也是回调
Continuation 就是“回调 → async”的官方桥梁,
核心思想:“记住挂起点,等回调来了再恢复。”
二、4 种函数一张表
函数 | 是否检查误用 | 是否支持 throws | 典型用途 |
---|---|---|---|
withUnsafeContinuation |
❌ | ❌ | 性能敏感、自己保证只 resume 一次 |
withCheckedContinuation |
✅ | ❌ | 开发阶段、调试逻辑 |
withUnsafeThrowingContinuation |
❌ | ✅ | 老回调用 Result /Error
|
withCheckedThrowingContinuation |
✅ | ✅ | 推荐默认,又能抛又能查 |
口诀:“开发用 Checked,上线改 Unsafe;要抛错就带 Throwing。”
三、最小例子:把 DispatchQueue 计时器变成 async
老回调版:
func oldTimer(seconds: Int, completion: @Sendable @escaping () -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(seconds)) {
completion()
}
}
- unsafe 版(最简)
func modernTimer(seconds: Int) async {
await withUnsafeContinuation { continuation in
oldTimer(seconds: seconds) {
continuation.resume() // 必须且只能调一次
}
}
}
- checked 版(开发推荐)
func safeTimer(seconds: Int) async {
await withCheckedContinuation { continuation in
oldTimer(seconds: seconds) {
continuation.resume()
// 如果这里手滑再 resume 一次 → 运行时直接 fatal + 清晰提示
}
}
}
错误示例:
SWIFT TASK CONTINUATION MISUSE: safeTimer(seconds:) tried to resume its continuation more than once
四、Throwing 版本:Network 回调 → async throws
老接口:
struct User {
let name: String
init(name: String) {
self.name = name
}
}
enum NetworkError: Error { case invalidID }
func fetchUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 0.3) {
id == "invalid" ? completion(.failure(NetworkError.invalidID))
: completion(.success(User(name: "A")))
}
}
- throwing + checked(默认选它)
func modernFetch(id: String) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
fetchUser(id: id) { result in
switch result {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
// 使用
Task {
do {
let user = try await modernFetch(id: "123")
print("Got", user.name)
} catch {
print("Error:", error)
}
}
- 如果回调用两个单独参数
(Data?, Error?)
:
continuation.resume(
with: Result(success: data, failure: error) // 方便构造
)
五、Actor 隔离专用参数 isolation
场景:在 @MainActor
类里发起网络,回来必须主线程刷新 UI。
@MainActor
class VM {
let label = UILabel()
func updateText() async {
let text = try? await withCheckedThrowingContinuation(
isolation: MainActor.shared // 👈 指定“恢复点”隔离域
) { continuation in
fetchString { continuation.resume(returning: $0) }
}
label.text = text // 保证在主线程
}
}
-
isolation:
让 resume 后的代码直接跑到指定 Actor - 省去一次
await MainActor.run { }
切换,更轻量 - 仅 Throwing 系列支持(Swift 6.0+)
六、常见坑 & Tips
-
必须且只能 resume 一次
忘记 → 任务永远挂起;多调 → 运行时
fatal
。 -
不要存储 continuation 到外部属性
生命周期仅限
{}
块,出来就 UB。 -
Task 取消感知
若业务需支持取消,请用
withTaskCancellationHandler
或AsyncStream
。 -
闭包捕获 self 小心循环引用
老回调里
weak self
的习惯继续保持。
七、一句话总结
Continuation = 回调 → async/await 的“官方翻译器”。
记住口诀:
“开发用 Checked,上线 Unsafe;要抛错就 Throwing;主线程回来加 isolation。”
用好这把桥,就能一口气把全项目的老回调全部现代化,同时享受编译期检查和运行时性能。