【问题标题】:SwiftUI Running Async Code Within Synchronous HandlerSwiftUI 在同步处理程序中运行异步代码
【发布时间】:2023-01-13 04:32:19
【问题描述】:
我正在创建一个游戏,在用户登录后,我想将他们的玩家 ID 发送到我的后端。由于这是在 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 函数不是异步函数,这是有道理的。
这里最好的方法是什么?我想等到获得 PlayerID 的值,然后再调用 await myBackendCall(playerID)。这里的任何建议将不胜感激,谢谢!
【问题讨论】:
标签:
swift
asynchronous
swiftui
async-await
game-center
【解决方案1】:
要使完成处理程序 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
}
}
【解决方案2】:
当你有一个回调闭包时(比如authenticateHandler),它总是意味着这个闭包可能会被调用多次。合适的 async-await 模式应该是 AsyncSequence(例如,AsyncStream 或 AsyncThrowingStream)。
因此,您可以将 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)是为完成处理程序模式设计的,它必须被调用一次,而且只能调用一次。如果您使用经过检查的延续并再次调用闭包,it will be“记录正确性违规”,因为“您必须在整个程序的每个执行路径上恰好调用一次resume方法。”
相反,在可能多次调用它的情况下,请考虑将其作为异步序列处理。