NavigationStack中SignInWithAppleButton与scenePhase冲突致登录失败的解决办法
问题现象
将应用引导流程切换为SwiftUI新的NavigationStack后,包含SignInWithAppleButton的登录视图会自动弹出(导航路径被意外重置),导致登录完成后的回调函数无法触发。但移除App结构体中的@Environment(\.scenePhase)声明后,功能完全正常,且相同代码在NavigationStack外运行无异常。
复现代码
import SwiftUI @main struct TestNavSignInApp: App { // 注释掉此行则问题消失,保留则登录视图弹出 @Environment(\.scenePhase) private var scenePhase var body: some Scene { WindowGroup { ContentView() } } } struct ContentView: View { @ObservedObject var onboarding = OnboardingFlow() var body: some View { NavigationStack(path: $onboarding.path) { Button("Start") { onboarding.start() } .navigationDestination(for: OnboardDestination.self) { destination in ViewFactory.viewForDestination(destination) } } .environmentObject(onboarding) } } enum OnboardDestination { case welcomPage case loginPage } class ViewFactory { @ViewBuilder static func viewForDestination(_ destination: OnboardDestination) -> some View { switch destination { case .welcomPage: WelcomeView() case .loginPage: SignInWithAppleView() } } } class OnboardingFlow: ObservableObject { @Published var path = NavigationPath() func gotoHomePage() { path.removeLast(path.count) } func gotoPrev() { path.removeLast() } func start() { path.append(OnboardDestination.welcomPage) } func next() { path.append(OnboardDestination.loginPage) } } struct WelcomeView: View { @EnvironmentObject var onboarding: OnboardingFlow var body: some View { Button("Next") { onboarding.next() } .navigationBarBackButtonHidden(true) } } struct SignInWithAppleView: View { @EnvironmentObject var onboarding: OnboardingFlow var body: some View { VStack(spacing: 18) { SignInWithAppleButton( .signIn, onRequest: configure, onCompletion: handle ) } .navigationBarBackButtonHidden(true) } func configure(_ request: ASAuthorizationAppleIDRequest) { request.requestedScopes = [.email, .fullName] } func handle(_ authResult: Result<ASAuthorization, Error>) { print("Handle:") switch authResult { case.success(let auth): print("Authorization successful: \(auth)") switch auth.credential { case let credential as ASAuthorizationAppleIDCredential: print("credential: \(credential)") default: print("Authorization default credential: \(auth.credential.description)") } case.failure(let error): print("Authorization failed:error: \(error.localizedDescription)") } } }
问题原因
这是SwiftUI的已知兼容性问题:当在App层级直接监听scenePhase时,SignInWithAppleButton触发的系统登录弹窗会导致scenePhase临时切换(例如从active变为background再回到active),进而触发App结构体的body重计算。此时NavigationStack绑定的NavigationPath会因父视图的重渲染意外重置,导致当前登录视图被弹出。
可行替代方案
1. 在子视图中监听scenePhase
不在App结构体直接声明scenePhase,而是将监听逻辑移到子视图中,通过.onChange(of: scenePhase)处理,避免触发App层级的整体重计算:
struct ContentView: View { @StateObject var onboarding = OnboardingFlow() @Environment(\.scenePhase) private var scenePhase var body: some View { NavigationStack(path: $onboarding.path) { Button("Start") { onboarding.start() } .navigationDestination(for: OnboardDestination.self) { destination in ViewFactory.viewForDestination(destination) } } .environmentObject(onboarding) .onChange(of: scenePhase) { newPhase in // 在这里处理场景生命周期变化逻辑 switch newPhase { case .active: // App回到前台逻辑 break case .inactive: // App进入非活跃状态逻辑 break case .background: // App进入后台逻辑 break @unknown default: break } } } }
2. 用NotificationCenter替代scenePhase监听
创建独立的状态管理器,通过系统通知监听App生命周期,完全避免使用@Environment(\.scenePhase):
class ScenePhaseManager: ObservableObject { @Published var currentPhase: ScenePhase = .active init() { let center = NotificationCenter.default center.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { _ in self.currentPhase = .active } center.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { _ in self.currentPhase = .inactive } center.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { _ in self.currentPhase = .background } } } // 在App中注入管理器 @main struct TestNavSignInApp: App { @StateObject private var phaseManager = ScenePhaseManager() var body: some Scene { WindowGroup { ContentView() .environmentObject(phaseManager) } } } // 在需要监听的视图中使用 struct SomeView: View { @EnvironmentObject var phaseManager: ScenePhaseManager var body: some View { Text("Sample View") .onChange(of: phaseManager.currentPhase) { newPhase in // 处理生命周期变化 } } }
3. 优化NavigationPath的状态稳定性
将ContentView中的OnboardingFlow从@ObservedObject改为@StateObject,确保导航路径状态不会因父视图重计算被意外重置:
struct ContentView: View { @StateObject var onboarding = OnboardingFlow() // 替换@ObservedObject var body: some View { NavigationStack(path: $onboarding.path) { // 原有视图内容 } .environmentObject(onboarding) } }
这个改动配合子视图监听scenePhase的方案,能最大化减少状态异常的概率。
内容的提问来源于stack exchange,提问作者Todd Hoff
相关产品推荐
相关产品推荐

