SwiftUI在同步处理程序中运行异步代码



我正在创建一个游戏,在用户登录后,我想将他们的playerID发送到我的后端。因为这是在SwiftUI,我有以下(顺便说一句,我知道我们不应该再使用playerID了,但这只是一个最小的可重复的例子):

import SwiftUI
import GameKit
struct SampleView: View {
let localPlayer = GKLocalPlayer.local

func authenticateUser() async {
localPlayer.authenticateHandler = { vc, error in
guard error == nil else {
print(error?.localizedDescription ?? "")
return
}
if localPlayer.isAuthenticated {
let playerID = localPlayer.playerID
GKAccessPoint.shared.isActive = localPlayer.isAuthenticated
// here is where I would like to make an async call
}
}
}
var body: some View {
VStack {
Text("Sample View")
}
.task {
await authenticateUser()
}
}
}
struct SampleView_Previews: PreviewProvider {
static var previews: some View {
SampleView()
}
}

在指示我想放置异步调用的注释中,我尝试了类似的东西await myBackendCall(playerID)但是这会抛出错误

Invalid conversion from 'async' function of type '(UIViewController?, (any Error)?) async -> Void' to synchronous function type '(UIViewController?, (any Error)?) -> Void'

这是有意义的,因为authenticateHandler函数不是async函数。

这里最好的方法是什么?我想等到我有PlayerID的值,然后调用await myBackendCall(playerID)。任何建议,这里将非常感激,谢谢!

要使完成处理程序async使用延续,如果用户通过身份验证,则返回true,否则返回false

func authenticateUser() async -> Bool {
return await withCheckedContinuation { continuation in
localPlayer.authenticateHandler = { vc, error in
if let error {
print(error.localizedDescription)
continuation.resume(returning: false)
} else {
continuation.resume(returning: localPlayer.isAuthenticated)
}
}
}
}

和在task范围内写

.task {
let isAuthenticated = await authenticateUser()
if isAuthenticated {
let playerID = localPlayer.playerID
GKAccessPoint.shared.isActive = localPlayer.isAuthenticated
// here is where I would like to make an async call
}
}

当您有一个回调闭包(如authenticateHandler)时,它总是意味着闭包可能被多次调用。合适的async-await模式应该是AsyncSequence(例如,AsyncStreamAsyncThrowingStream)。

因此,您可以将authenticateHandler封装在异步序列中,如下所示:
func viewControllers() -> AsyncThrowingStream<UIViewController, Error> {
AsyncThrowingStream<UIViewController, Error> { continuation in
GKLocalPlayer.local.authenticateHandler = { viewController, error in
if let viewController {
continuation.yield(viewController)
} else {
continuation.finish(throwing: error ?? GKError(.unknown))
}
}
}
}

然后你可以这样做:

.task {
do {
for try await _ in viewControllers() {
GKAccessPoint.shared.isActive = GKLocalPlayer.local.isAuthenticated
// do your subsequent `async` call here
}
} catch {
GKAccessPoint.shared.isActive = false
print(error.localizedDescription)
}
}

有关更多信息,请参阅WWDC 2021视频Meet AsyncSequence。但是,withCheckedContinuation(或withThrowingCheckedContinuation)是为完成处理程序模式设计的,它必须被调用一次,而且只能调用一次。如果你使用一个检查过的延续并且闭包被再次调用,它将是"记录正确性违规",因为"你必须在整个程序的每个执行路径上精确地调用一次resume方法。">

相反,在可能被多次调用的情况下,考虑将其作为异步序列处理。

最新更新