swiftswiftuiswiftui-navigationviewswiftui-navigationstack

SwiftUI Loader Not Covering Navigation Bar in UIKit Navigation Setup


I'm encountering an issue when trying to present a loader over my views. The loader fails to cover the navigation, allowing users to interact with the navigation elements, such as tapping the back button, which creates an awkward user experience.

It's important to note that while all my views are built using SwiftUI, I'm using UIKit-based navigation. Our application needs to support older iOS versions, and our team has decided to use UIKit navigation for its flexibility and maturity compared to SwiftUI navigation.

How can I ensure the loader covers the entire view, including the navigation bar, in this mixed SwiftUI and UIKit environment? Any advice or solutions would be greatly appreciated.

In the scenario above:

  1. The LoaderView covers the main content but not the navigation bar.
  2. When the loader is active, the navigation bar elements remain interactive, which is not the desired behavior, as loader needs to be cover fully.

ActivityIndicatorView(Loader)

public struct ActivityIndicatorView<Content>: View where Content: View {

    public var isActive: Bool
    public var content: () -> Content

    public init(isActive: Bool, content: @escaping () -> Content) {
        self.isActive = isActive
        self.content = content
    }

    public var body: some View {
        self.content()
            .disabled(self.isActive)
            .background(self.isActive ? Color.black.opacity(0.25) : .clear)
            .blur(radius: self.isActive ? 3 : 0)
            .overlay {
                if isActive {
                    Image("Loader/Container", bundle: .shamrockCore)
                        .overlay {
                            ActivityIndicator(isAnimating: self.isActive, style: .large)
                        }
                        .zIndex(1)
                }
            }
    }

}

PasswordRecoveryView

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 {
        
        ActivityIndicatorView(isActive: true) {
            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.$didForgotPasswordEmailGenerated) { isEmailGenerated in
                if isEmailGenerated {
                    coordinator.goToPasswordRecoveryConfirmationView()
                }
            }
        }
    }
}

enter image description here


Solution

  • Loader is covering only content of current pushed view's body. you can make separate loader class for this purpose that's how it can be used entirely to anywhere:

    Loader Class

    class Loader {
        static var spinner: UIActivityIndicatorView?
    
    static func show() {
        guard let window = UIApplication.shared.windows.first else { return }
    
        let spinnerView = UIView(frame: window.bounds)
        spinnerView.backgroundColor = UIColor(white: 0, alpha: 0.5)
    
        let activityIndicator = UIActivityIndicatorView(style: .large)
        activityIndicator.center = spinnerView.center
        activityIndicator.startAnimating()
    
        spinnerView.addSubview(activityIndicator)
        window.addSubview(spinnerView)
    
        spinner = activityIndicator
    }
    
    static func hide() {
        spinner?.superview?.removeFromSuperview()
        spinner = nil
    } }