SwiftUI加载器在UIKit导航架构下无法覆盖导航栏问题
SwiftUI混合UIKit导航时实现全屏覆盖加载器的解决方案
问题背景
使用SwiftUI构建视图,但依赖UIKit导航控制器(为兼容旧版iOS),当前加载器仅覆盖SwiftUI主内容区域,无法覆盖UIKit导航栏,导致加载状态下导航栏元素仍可交互,影响用户体验。
核心原因
UIKit导航栏属于导航控制器的独立层级,和SwiftUI视图的渲染层级相互分离,SwiftUI的overlay、background等修饰符仅能作用于自身视图范围,无法触及UIKit管理的导航栏区域。
解决方案
方案一:自定义UIKit导航控制器统一控制加载器
通过在导航控制器层面添加全屏加载视图,直接覆盖包括导航栏在内的整个屏幕,同时统一管理交互禁用状态。
1. 自定义导航控制器子类
class CustomNavigationController: UINavigationController { // 全屏加载视图 private let loaderView: UIView = { let view = UIView() view.backgroundColor = UIColor.black.withAlphaComponent(0.25) view.isHidden = true view.translatesAutoresizingMaskIntoConstraints = false // 添加系统活动指示器 let activityIndicator = UIActivityIndicatorView(style: .large) activityIndicator.translatesAutoresizingMaskIntoConstraints = false activityIndicator.startAnimating() view.addSubview(activityIndicator) // 约束配置 NSLayoutConstraint.activate([ activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor), view.topAnchor.constraint(equalTo: view.superview!.topAnchor), view.leadingAnchor.constraint(equalTo: view.superview!.leadingAnchor), view.trailingAnchor.constraint(equalTo: view.superview!.trailingAnchor), view.bottomAnchor.constraint(equalTo: view.superview!.bottomAnchor) ]) return view }() override func viewDidLoad() { super.viewDidLoad() // 将加载视图添加到导航控制器的根视图 view.addSubview(loaderView) } // 显示加载器 func showLoader() { loaderView.isHidden = false // 禁用整个导航控制器的交互 view.isUserInteractionEnabled = false } // 隐藏加载器 func hideLoader() { loaderView.isHidden = true view.isUserInteractionEnabled = true } }
2. 在SwiftUI视图中调用加载器控制方法
首先在ViewModel中添加加载状态的发布属性:
class PasswordRecoveryViewModel: ObservableObject { @Published var isLoading: Bool = false // ... 其他属性和方法 }
然后在SwiftUI视图中监听状态变化,通过协调器获取导航控制器并调用方法:
struct PasswordRecoveryView: View { private(set) var coordinator: LoginCordinator @StateObject private var viewModel = PasswordRecoveryViewModel() private var confimrationEmailButton: some View { // Submit Button View } private var emailField: some View { // Email View } var body: some View { ZStack { Theme.PrimaryBackground .ignoresSafeArea() VStack { VStack(spacing: 40) { Text(L.localize(.enter_email_text)) .multilineTextAlignment(.center) .foregroundColor(Theme.TintPrimaryLight) .font(.metropolis(.regular, size: 20)) emailField } .padding(.horizontal, 30) .padding(.top, 15) Spacer() confimrationEmailButton .padding(.bottom) } .navigationBarHidden(false) .navigationTitle(L.localize(.forgot_password_screen_title)) .onReceive(viewModel.$isLoading) { isLoading in // 转换为自定义导航控制器并调用加载方法 guard let navController = coordinator.navigationController as? CustomNavigationController else { return } isLoading ? navController.showLoader() : navController.hideLoader() } .onReceive(viewModel.$didForgotPasswordEmailGenerated) { isEmailGenerated in if isEmailGenerated { coordinator.goToPasswordRecoveryConfirmationView() } } } } }
方案二:使用UIViewControllerRepresentable实现全屏加载器
通过UIKit的Window层级添加加载视图,直接覆盖整个屏幕,无需修改导航控制器。
1. 实现全屏加载器的UIViewControllerRepresentable
struct FullScreenLoader: UIViewControllerRepresentable { var isActive: Bool func makeUIViewController(context: Context) -> UIViewController { let vc = UIViewController() vc.view.backgroundColor = .clear return vc } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { // 获取当前活跃的KeyWindow guard let window = UIApplication.shared.connectedScenes .filter({ $0.activationState == .foregroundActive }) .compactMap({ $0 as? UIWindowScene }) .first?.windows .first(where: { $0.isKeyWindow }) else { return } if isActive { // 避免重复添加 guard window.viewWithTag(999) == nil else { return } let loaderView = UIView() loaderView.tag = 999 loaderView.backgroundColor = UIColor.black.withAlphaComponent(0.25) loaderView.frame = window.bounds let activityIndicator = UIActivityIndicatorView(style: .large) activityIndicator.center = loaderView.center activityIndicator.startAnimating() loaderView.addSubview(activityIndicator) window.addSubview(loaderView) } else { // 移除加载视图 window.viewWithTag(999)?.removeFromSuperview() } } }
2. 在SwiftUI视图中集成
struct PasswordRecoveryView: View { // ... 现有属性 var body: some View { ZStack { // 原有内容视图 ZStack { Theme.PrimaryBackground .ignoresSafeArea() VStack { // ... 内容部分 } .navigationBarHidden(false) .navigationTitle(L.localize(.forgot_password_screen_title)) .onReceive(viewModel.$didForgotPasswordEmailGenerated) { isEmailGenerated in if isEmailGenerated { coordinator.goToPasswordRecoveryConfirmationView() } } } // 全屏加载器,置于最顶层 FullScreenLoader(isActive: viewModel.isLoading) .ignoresSafeArea() } } }
方案对比
- 方案一:更符合UIKit导航的架构设计,统一管理加载状态,避免多个页面重复实现,适合大型项目长期维护。
- 方案二:无需修改导航控制器,实现简单快速,适合小型场景或临时需求,但需注意Window层级的兼容性。
内容的提问来源于stack exchange,提问作者Ahtazaz
相关产品推荐
相关产品推荐

