swiftasynchronousswiftuiasync-awaitgame-center

SwiftUI Running Async Code Within Synchronous Handler


I am creating a game where, after a user signs in, I want to send their playerID to my backend. Since this is in SwiftUI, I have the following (btw I know we're not supposed to be using playerID anymore but this is just a minimal reproducible example):

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()
    }
}

In the comment indicating where I'd like to place an async call, I have tried something like await myBackendCall(playerID) but this throws the error

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

which makes sense given that the authenticateHandler function isn't an async function.

What is the best approach here? I'd like to wait until I have the value for PlayerID, and then call await myBackendCall(playerID). Any advice here would be much appreciated, thank you!


Solution

  • To make a completion handler async use a continuation, it returns true if the user is authenticated, otherwise 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)
                }
            }
        }
     }
    

    and in the task scope write

    .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
        }
    }