First of all, this is by no means a perfect example, but rather an idea of how it can be implemented. For example, to make it easier to show, I am using @AppStorage and the ID to save it here.
I had the same Issue today, … in tvOS, the SignInWithAppleButton does not trigger its closures. It only renders the required visual appearance of the button and haptics/animations.
How to fix this?
I used the official SignInWithAppleButton and attached an .onTapGesture that launches a custom ASAuthorizationController with my own ASAuthorizationControllerDelegate, as the button does not trigger its built-in request or completion handlers under tvOS.
Example (the Button)
SignInWithAppleButton { _ in } onCompletion: { _ in }
.onTapGesture {
Task { await viewModel.signInWithApple() }
}
Example ViewModel
import Combine
import SwiftUI
@MainActor
class ViewModel: ObservableObject {
@AppStorage("signInWithAppleUserIdString") var signInWithAppleUserIdString: String = ""
var appleSignInManager = AppleSignInManager()
func signInWithApple() async {
let appleIdString = await appleSignInManager.signIn()
if let appleIdString {
signInWithAppleUserIdString = appleIdString
} else {
print("ERROR: USER NOT SIGNED IN WITH APPLE")
}
}
func signOutFromApple() {
signInWithAppleUserIdString = ""
}
}
Example Class
I called it AppleSignInManager because it's simple, but that's roughly how you could create it.
import AuthenticationServices
import Combine
import SwiftUI
@MainActor
final class AppleSignInManager: NSObject, ObservableObject,
ASAuthorizationControllerDelegate,
ASAuthorizationControllerPresentationContextProviding {
private var continuation: CheckedContinuation<String?, Never>?
override init() {
super.init()
}
func signIn() async -> String? {
return await withCheckedContinuation { continuation in
self.continuation = continuation
startAuthorization()
}
}
private func startAuthorization() {
let provider = ASAuthorizationAppleIDProvider()
let request = provider.createRequest()
request.requestedScopes = []
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
if let keyWindow = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.flatMap({ $0.windows })
.first(where: { $0.isKeyWindow }) {
return keyWindow
}
if let windowScene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first {
return ASPresentationAnchor(windowScene: windowScene)
}
fatalError("NO WINDOW SCENE FOUND")
}
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
if let credential = authorization.credential as? ASAuthorizationAppleIDCredential {
let userId = credential.user
continuation?.resume(returning: userId)
continuation = nil
} else {
continuation?.resume(returning: nil)
continuation = nil
}
}
func authorizationController(controller: ASAuthorizationController,
didCompleteWithError error: Error) {
print("ERROR:", error.localizedDescription)
continuation?.resume(returning: nil)
continuation = nil
}
}
Explanation
My ViewModel stores the user ID returned by the “Sign in with Apple” authorization process and is directly linked to the custom ASAuthorizationControllerDelegate, which provides the result.