You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 06:05:28