Mac Catalyst应用Sign In with Apple崩溃:视图不在层级无法弹授权控制器
问题根因
这个崩溃是Mac Catalyst平台下SwiftUI原生SignInWithAppleButton组件的已知兼容性缺陷,和Firebase SDK逻辑无关:
- iOS/iPadOS端的
SignInWithAppleButton内部没有强校验视图所属层级,只要按钮被添加到视图树就能正常触发授权弹窗 - Mac Catalyst端的同组件加了强制校验逻辑:点击按钮时如果检测到当前按钮的视图层级锚点找不到对应窗口的根控制器,就会直接触发fatalError,抛出你看到的崩溃日志
- 你的代码里存在两个会直接触发该问题的错误:
- 把
@EnvironmentObject var store: Store写在了SignInWithAppleButtonView结构体声明的外面,会导致视图重绘时环境对象注入紊乱,直接导致Catalyst下视图层级判定失败 - 把按钮嵌套在
GeometryReader+ZStack的深层子视图位置,Catalyst的布局逻辑会在重绘时短暂把按钮判定为不在视图层级内
- 把
- 额外提一句:你写的
onCompletion回调里switch语法存在括号错位问题,case .failure被写到了switch块外面,改完记得修正,否则会编译报错。
修复方案
按优先级操作,第一步改完就能解决90%的同类场景崩溃:
方案一:最小改动修复(无需替换组件)
- 把
SignInWithAppleButtonView文件中、结构体声明外面的那行@EnvironmentObject var store: Store移到结构体内部作为属性,删除登录页里给按钮单独加的.environmentObject(store)——外层视图注入过的环境对象,子视图会自动继承,重复注入反而会导致层级紊乱。 - 调整登录页布局,不要把Apple登录按钮包在
GeometryReader的深层嵌套里,给按钮加固定id避免系统布局时把它判定为临时复用视图,修改后的登录页核心代码参考:
struct login: View { @EnvironmentObject var store: Store var body: some View { VStack(alignment: .center, spacing: 20) { SignInWithAppleButtonView() .signInWithAppleButtonStyle(.black) .id("fixed_apple_signin_button") } .frame(maxWidth: .infinity, maxHeight: .infinity) .background( // 原来GeometryReader负责的背景布局逻辑移到这里 ) } }
- 修正按钮回调里的
switch语法错误,把case .failure移到switch块内部。
方案二:100%兼容方案(适配所有macOS版本)
SwiftUI原生SignInWithAppleButton在Mac Catalyst下的层级校验bug在部分低版本macOS上仍存在,最稳妥的方案是用UIViewRepresentable包装UIKit原生的ASAuthorizationAppleIDButton,完全绕开SwiftUI组件的内部校验逻辑,全端兼容:
import SwiftUI import AuthenticationServices import CryptoKit struct CompatibleAppleSignInButton: UIViewRepresentable { let onRequest: (ASAuthorizationAppleIDRequest, String) -> Void let onCompletion: (Result<ASAuthorization, Error>, String?) -> Void func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIView(context: Context) -> ASAuthorizationAppleIDButton { let button = ASAuthorizationAppleIDButton(type: .signIn, style: .black) button.addTarget(context.coordinator, action: #selector(Coordinator.triggerSignIn), for: .touchUpInside) return button } func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {} class Coordinator: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { let parent: CompatibleAppleSignInButton var currentNonce: String? init(_ parent: CompatibleAppleSignInButton) { self.parent = parent super.init() } @objc func triggerSignIn() { let nonce = randomNonceString() currentNonce = nonce let request = ASAuthorizationAppleIDProvider().createRequest() request.requestedScopes = [.fullName, .email] request.nonce = sha256(nonce) parent.onRequest(request, nonce) let authController = ASAuthorizationController(authorizationRequests: [request]) authController.delegate = self authController.presentationContextProvider = self authController.performRequests() } func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { guard let keyWindow = UIApplication.shared.connectedScenes .filter({ $0.activationState == .foregroundActive }) .compactMap({ $0 as? UIWindowScene }) .flatMap({ $0.windows }) .first(where: { $0.isKeyWindow }) else { return UIWindow() } return keyWindow } func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { parent.onCompletion(.success(authorization), currentNonce) } func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { parent.onCompletion(.failure(error), currentNonce) } 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() } private func randomNonceString(length: Int = 32) -> String { precondition(length > 0) let charset: [Character] = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._") var result = "" var remaining = length while remaining > 0 { let randoms: [UInt8] = (0..<16).map { _ in var val: UInt8 = 0 let status = SecRandomCopyBytes(kSecRandomDefault, 1, &val) guard status == errSecSuccess else { fatalError("Nonce generate failed, osstatus: \(status)") } return val } randoms.forEach { rand in guard remaining > 0 else { return } if rand < charset.count { result.append(charset[Int(rand)]) remaining -= 1 } } } return result } } }
替换原来的SwiftUI原生SignInWithAppleButton即可,回调参数和你原来的逻辑完全对齐,拿到授权凭证后直接调用Store类里的signInWithAppleHandler方法就行,Firebase侧的逻辑不需要任何修改。
避坑提示
- 不要在Mac Catalyst端给Apple登录按钮加
.opacity(0<x<1)、.offset这类会改变视图实际渲染位置/透明度的修饰符,也会触发组件内部的层级校验失败 - 如果登录页是通过Sheet、弹窗形式弹出的,要等弹窗动画完全结束再渲染登录按钮,避免动画过程中点击按钮触发层级判定错误。
内容的提问来源于stack exchange,提问作者RileyDev
相关产品推荐
相关产品推荐

