如何让基于ASAuthorizationController的Apple登录异步返回Bool?
问题描述
想让使用ASAuthorizationController的Apple登录函数返回Bool类型值,试过withCheckedThrowingContinuation但没搞定,不想用完成处理程序,更想用async实现。现有代码如下:
func signInWithApple(withState state: SignInState) async -> Bool { let request = signInWithAppleRequest(withState: state) let authorizationController = ASAuthorizationController(authorizationRequests: [request]) authorizationController.delegate = self authorizationController.performRequests() } func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { //... let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce) switch state { case .signIn: Task { authenticationState = .authenticating do { let result = try await Auth.auth().signIn(with: credential) if result.additionalUserInfo?.isNewUser == true { self.isNewUser = true } return true } catch { self.authenticationState = .unauthenticated print(error.localizedDescription) return false } } case .reauth: Task { if let user = Auth.auth().currentUser { do { try await user.reauthenticate(with: credential) return true } catch { print(error.localizedDescription) return false } } } } } }
解决方案
要实现async返回Bool,核心是把ASAuthorizationController的delegate回调转成async/await形式,这里用withCheckedThrowingContinuation(需处理错误)或者withCheckedContinuation(仅返回成功/失败)即可,以下是完整修改示例:
import AuthenticationServices import FirebaseAuth class YourAuthManager: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { // 暂存async任务的continuation private var signInContinuation: CheckedContinuation<Bool, Error>? func signInWithApple(withState state: SignInState) async throws -> Bool { return try await withCheckedThrowingContinuation { [weak self] continuation in guard let self = self else { continuation.resume(throwing: NSError(domain: "AuthError", code: -1, userInfo: [NSLocalizedDescriptionKey: "实例已释放"])) return } // 保存continuation,等待回调触发 self.signInContinuation = continuation let request = self.signInWithAppleRequest(withState: state) let authorizationController = ASAuthorizationController(authorizationRequests: [request]) authorizationController.delegate = self authorizationController.presentationContextProvider = self authorizationController.performRequests() } } // Apple登录成功回调 func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { guard let continuation = signInContinuation else { return } signInContinuation = nil // 用完清空,避免内存泄漏 // 解析Apple凭证 guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential, let idTokenData = appleIDCredential.identityToken, let idTokenString = String(data: idTokenData, encoding: .utf8), let nonce = currentNonce else { continuation.resume(throwing: NSError(domain: "AuthError", code: -2, userInfo: [NSLocalizedDescriptionKey: "Apple凭证无效"])) return } let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce) // 处理登录/重认证逻辑 Task { do { switch self.currentSignInState { // 存下传入的state供回调使用 case .signIn: let result = try await Auth.auth().signIn(with: credential) self.isNewUser = result.additionalUserInfo?.isNewUser == true self.authenticationState = .authenticated continuation.resume(returning: true) case .reauth: guard let user = Auth.auth().currentUser else { throw NSError(domain: "AuthError", code: -3, userInfo: [NSLocalizedDescriptionKey: "当前无登录用户"]) } try await user.reauthenticate(with: credential) continuation.resume(returning: true) } } catch { // 处理Firebase认证错误 if self.currentSignInState == .signIn { self.authenticationState = .unauthenticated } print(error.localizedDescription) continuation.resume(throwing: error) } } } // Apple登录失败回调 func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { guard let continuation = signInContinuation else { return } signInContinuation = nil continuation.resume(throwing: error) } // 必须实现,否则Apple登录弹窗无法弹出 func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { // 获取当前活跃窗口 guard let windowScene = UIApplication.shared.connectedScenes .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene, let window = windowScene.windows.first else { return ASPresentationAnchor() } return window } // 生成Apple登录请求的辅助方法 private func signInWithAppleRequest(withState state: SignInState) -> ASAuthorizationAppleIDRequest { let request = ASAuthorizationAppleIDProvider().createRequest() request.requestedScopes = [.fullName, .email] // 生成并保存nonce(Apple登录安全验证要求) currentNonce = randomNonceString() request.nonce = sha256(currentNonce!) // 保存当前登录状态 currentSignInState = state return request } // 辅助变量 private var currentNonce: String? private var currentSignInState: SignInState = .signIn var authenticationState: AuthenticationState = .unauthenticated var isNewUser = false // 生成随机nonce的工具方法 private func randomNonceString(length: Int = 32) -> String { precondition(length > 0) let charset = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._") var result = "" var remainingLength = length while remainingLength > 0 { var randomBytes = [UInt8](repeating: 0, count: 16) let status = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes) guard status == errSecSuccess else { fatalError("生成nonce失败,错误码:\(status)") } for byte in randomBytes { if remainingLength == 0 { break } if byte < UInt8(charset.count) { result.append(charset[Int(byte)]) remainingLength -= 1 } } } return result } // SHA256加密nonce的工具方法 private func sha256(_ input: String) -> String { let inputData = Data(input.utf8) let hashedData = SHA256.hash(data: inputData) return hashedData.compactMap { String(format: "%02x", $0) }.joined() } } // 自定义枚举类型 enum SignInState { case signIn, reauth } enum AuthenticationState { case unauthenticated, authenticating, authenticated }
重点提醒
- 用
withCheckedThrowingContinuation将delegate回调转换为async/await形式,即可让函数返回Bool;若无需抛出错误,可改用withCheckedContinuation<Bool, Never>。 - 必须保存continuation引用,直到回调触发后调用
resume,同时用[weak self]避免循环引用。 - 必须实现
ASAuthorizationControllerPresentationContextProviding协议的presentationAnchor方法,否则Apple登录弹窗无法显示。 - 成功和失败回调中都要调用
resume,防止async任务永久挂起。 - Firebase异步操作需在Task内执行,完成后再通过continuation返回结果或错误。
内容的提问来源于stack exchange,提问作者Dan
相关产品推荐
相关产品推荐

