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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 21:24:52