swiftasynchronousasync-awaitcombinegame-center

Swift task continuation misuse; function tried to resume its continuation more than once


Here is a minimal reproducible example of my setup:

import SwiftUI
import GameKit
import Combine

struct ContentView: View {
    var body: some View {
        Text("hello")
            .task {
                await authenticateUser()
            }
    }

    func authenticateUser() async -> Bool {
        return await withCheckedContinuation { continuation in
            GKLocalPlayer.local.authenticateHandler = { vc, error in
                if let error {
                    continuation.resume(returning: false)
                } else {
                    GKAccessPoint.shared.isActive = GKLocalPlayer.local.isAuthenticated
                    continuation.resume(returning: GKLocalPlayer.local.isAuthenticated)
                }
            }
        }
    }
}

I'm using an async method here because I want to wait for authenticateUser to finish before moving on to other code, and this works great, but if I soft close the app and re-open it, I am hit with the error

_Concurrency/CheckedContinuation.swift:164: Fatal error: SWIFT TASK
CONTINUATION MISUSE: authenticateUser() tried to resume its
continuation more than once, returning false!

So somehow, it seems like the continuation is trying to resume each time the view is reloaded. I'm not sure why this is/how to prevent it, so any help here would be greatly appreciated, thank you!


Solution

  • As the GKLocalPlayer documentation says regarding this authenticateHandler:

    … implement this method to handle the multiple times GameKit invokes it during the sign-in process

    Because the authentication handler can be called multiple times, we would reach for an AsyncSequence, or, more specifically in this case, an AsyncStream:

    func authentications() -> AsyncThrowingStream<Bool, Error> {
        AsyncThrowingStream { continuation in
            GKLocalPlayer.local.authenticateHandler = { _, error in
                if let error {
                    continuation.finish(throwing: error)
                } else {
                    GKAccessPoint.shared.isActive = GKLocalPlayer.local.isAuthenticated
                    continuation.yield(GKLocalPlayer.local.isAuthenticated)
                }
            }
        }
    }
    

    Then you can iterate over that AsyncSequence:

    .task {
        do {
            for try await isAuthenticated in authentications() {
                print(isAuthenticated)
            }
        } catch {
            print(error)
        }
    }
    

    We cannot be more specific than that without knowing how you plan on using this authentication data. But the general idea is that if you are going to possibly yield more than one value, wrap it in a sequence.