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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 02:35:19