2025-02-07 17:13:18
,某些文章具有时效性,若有错误或已失效,请在下方留言。Many of Apple’s frameworks report back success or failure using multiple different delegate callback methods rather than completion handlers, which means a simple continuation won’t work.
许多 Apple 框架使用多个不同的委托回调方法而不是完成处理程序来报告成功或失败,这意味着简单的 continuation 将不起作用。
As a simple example, if you were implementing WKNavigationDelegate
to handle navigating around a WKWebView
you would implement methods like this:
举个简单的例子,如果你正在实施 WKNavigationDelegate
来处理 WKWebView
的导航,你将实现如下方法:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// our work succeeded
}
func webView(WKWebView, didFail: WKNavigation!, withError: any Error) {
// our work failed
}
So, rather than receiving the result of our work through a single completion closure, we instead get the result in two different places. In this situation we need to do a little more work to create async functions using continuations, because we need to be able to resume the continuation in either method.
因此,我们不是通过单个 completion closure 接收我们的工作结果,而是在两个不同的地方获得结果。在这种情况下,我们需要做更多的工作来使用 continuations 创建异步函数,因为我们需要能够在任一方法中恢复 continuation。
To solve this problem you need to know that continuations are just structs with a specific generic type. For example, a checked continuation that succeeds with a string and never throws an error has the type CheckedContinuation<String, Never>
, and an unchecked continuation that returns an integer array and can throw errors has the type UnsafeContinuation<[Int], Error>
.
要解决此问题,您需要知道 continuations 只是具有特定泛型类型的结构。例如,成功使用字符串且从不引发错误的已检查延续具有类型 CheckedContinuation<String, Never>
,而返回整数数组并可能引发错误的未检查延续具有类型 UnsafeContinuation<[Int], Error>
。
All this is important because to solve our delegate callback problem we need to stash away a continuation in one method – when we trigger some functionality – then resume it from different methods based on whether our code succeeds or fails.
所有这些都很重要,因为要解决我们的委托回调问题,我们需要在一个方法中存储一个延续 – 当我们触发某些功能时 – 然后根据我们的代码是成功还是失败从不同的方法中恢复它。
I want to demonstrate this using real code, so we’re going to create an @Observable
class to wrap Core Location, making it easier to request the user’s location.
我想使用真实代码来演示这一点,因此我们将创建一个 @Observable
类来包装 Core Location,从而更轻松地请求用户的位置。
First, add these imports to your code so we can read their location, and also use SwiftUI’s LocationButton
to get standardized UI:
首先,将这些导入添加到您的代码中,以便我们可以读取它们的位置,并使用 SwiftUI 的 LocationButton
来获得标准化的 UI:
import CoreLocation
import CoreLocationUI
Second, we’re going to create a small part of a LocationManager
class that has two properties: one for storing a continuation to track whether we have their location coordinate or an error, and one to track an instance of CLLocationManager
that does the work of finding the user. This also needs a small initializer so the CLLocationManager
knows to report location updates to us.
其次,我们将创建一个 LocationManager
类的一小部分,该类具有两个属性:一个用于存储延续以跟踪我们是具有位置坐标还是错误,另一个用于跟踪执行查找用户的 CLLocationManager
实例。这也需要一个小的初始化器,这样 CLLocationManager
就知道向我们报告位置更新。
Add this class now: 立即添加此类:
@Observable
class LocationManager: NSObject, CLLocationManagerDelegate {
var locationContinuation: CheckedContinuation<CLLocationCoordinate2D?, any Error>?
let manager = CLLocationManager()
override init() {
super.init()
manager.delegate = self
}
// More code to come
}
Third, we need to add an async function that requests the user’s location. This needs to be wrapped inside a withCheckedThrowingContinuation()
call, so that Swift creates a continuation we can stash away and use later. However, because this will be called from the main actor, we need to mark this method as also running on the main actor to avoid data races.
第三,我们需要添加一个请求用户位置的 async 函数。这需要包装在调用 withCheckedThrowingContinuation()
中,以便 Swift 创建一个我们可以存储起来并在以后使用的延续。但是,由于这将从主参与者调用,因此我们需要将此方法标记为也在主参与者上运行,以避免数据争用。
Add this method to the class now:
现在将此方法添加到类中:
@MainActor
func requestLocation() async throws -> CLLocationCoordinate2D? {
try await withCheckedThrowingContinuation { continuation in
locationContinuation = continuation
manager.requestLocation()
}
}
And finally we need to implement the two methods that might be called after we request the user’s location: didUpdateLocations
will be called if their location was received, and didFailWithError
otherwise. Both of these need to resume our continuation, with the former sending back the first location coordinate we were given, and the latter throwing whatever error occurred:
最后,我们需要实现在请求用户位置后可能调用的两种方法:如果收到用户的位置,则调用 didUpdateLocations
,否则调用 didFailWithError
。这两个都需要恢复我们的 continuation,前者发回我们得到的第一个位置坐标,而后者则抛出发生的任何错误:
Add these last two methods to the class now:
现在将最后两个方法添加到类中:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locationContinuation?.resume(returning: locations.first?.coordinate)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
locationContinuation?.resume(throwing: error)
}
So, by storing our continuation as a property we’re able to resume it in two different places – once where things go to plan, and once where things go wrong for whatever reason. Either way, no matter what happens our continuation resumes exactly once.
因此,通过将我们的延续存储为属性,我们可以在两个不同的地方恢复它 – 一次是事情按计划进行的地方,另一次是由于某种原因出错的地方。无论哪种方式,无论发生什么,我们的延续都会只恢复一次。
At this point our continuation wrapper is complete, so we can use it inside a SwiftUI view. If we put everything together, here’s the end result:
此时,我们的 continuation 包装器已完成,因此我们可以在 SwiftUI 视图中使用它。如果我们把所有东西放在一起,这就是最终结果:
@Observable
class LocationManager: NSObject, CLLocationManagerDelegate {
var locationContinuation: CheckedContinuation<CLLocationCoordinate2D?, any Error>?
let manager = CLLocationManager()
override init() {
super.init()
manager.delegate = self
}
@MainActor
func requestLocation() async throws -> CLLocationCoordinate2D? {
try await withCheckedThrowingContinuation { continuation in
locationContinuation = continuation
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locationContinuation?.resume(returning: locations.first?.coordinate)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
locationContinuation?.resume(throwing: error)
}
}
struct ContentView: View {
@State private var locationManager = LocationManager()
var body: some View {
LocationButton {
Task {
if let location = try? await locationManager.requestLocation() {
print("Location: \(location)")
} else {
print("Location unknown.")
}
}
}
.frame(height: 44)
.foregroundStyle(.white)
.clipShape(.capsule)
.padding()
}
}