【问题标题】: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(例如,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)是为完成处理程序模式设计的,它必须被调用一次,而且只能调用一次。如果您使用经过检查的延续并再次调用闭包,it will be“记录正确性违规”,因为“您必须在整个程序的每个执行路径上恰好调用一次resume方法。”

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-09-28
        • 1970-01-01
        • 2012-03-26
        • 2018-06-08
        • 2022-10-01
        相关资源
        最近更新 更多