Search code examples
iosswiftswiftui

Custom alert not performing any action


I have a requirement when any alert when triggered should show up on the top most view on the app. Referring some code on the internet for other views, I was able to come up with the solution which shows the alert on the view or even sheet if it is top most view.

However, buttons or any tap gesture on the alert doesn't work.

Note: I have a very complex app, so I created a quick sample app. If you copy my code and run it, the app will open a sheet automatically and then display an alert to show the kind of workflow I am performing in my app except in my app sheet opens on a manual button click.

Code:

import SwiftUI
import Foundation

struct ContentView: View {
    @StateObject var alertViewModel: AlertViewModel = AlertViewModel()
    @State private var showSheet: Bool = false
    
    var body: some View {
        NavigationStack {
            VStack(alignment: .center) {
                Text("This is the main view. Opening sheet view...").padding(.top, 100.0)
                Spacer()
            }
            .onAppear {
                Task {
                    try await Task.sleep(nanoseconds: 3_000_000_000)
                    showSheet = true
                    try await Task.sleep(nanoseconds: 3_000_000_000)
                    alertViewModel.showAlert = true
                }
            }
            .environmentObject(alertViewModel)
            .sheet(isPresented: $showSheet, content: {
                SheetA()
            })
            .overlayModal(isPresented: $alertViewModel.showAlert, modalContent: alertViewModel.alertView)
        }
    }
}

struct SheetA: View {
    var body: some View {
        VStack {
            Text("Sheet A to demo alert on top of all views. Alert will popup soon...").padding(.top, 100.0)
            Spacer()
        }
    }
}

// MARK: CUSTOM ALERT CODE

class AlertViewModel: ObservableObject {
    @Published public var showAlert: Bool = false
    
    func alertView() -> some View {
        return CustomAlertView()
    }
}

struct CustomAlertView: View {
    @State private var alertHostingController: UIHostingController<AnyView>?
    
    @EnvironmentObject var alertViewModel: AlertViewModel
 
    var body: some View {
        ZStack {
            Color.gray.opacity(0.3).ignoresSafeArea()
            VStack {
                
            }.onAppear {
                showAlert()
            }
        }
    }
    
    @ViewBuilder
    func alertContent() -> some View {
        GeometryReader { geometry in
            VStack {
                Image(systemName: "info.circle").resizable().frame(width: 20, height: 20)
                    .padding(.top, 30).foregroundColor(.blue)

                Text("Hello World").foregroundColor(.black).font(.title2).bold().multilineTextAlignment(.center)
                    .padding([.leading, .trailing], 20.0).padding(.top, 12.0)
                Spacer()
                
                Text("This is an alert to show detailed message for the app.").foregroundColor(Color.secondary).font(.body).multilineTextAlignment(.center)
                    .padding([.leading, .trailing], 20.0).padding(.top, 12.0)
                    .onTapGesture {
                        print("Testing alert tap on message")
                    }
                
                Spacer()
                
                Button {
                    print("Button tapped")
                } label: {
                    Text("Click Me").buttonBorderShape(.roundedRectangle)
                }
                .padding(.bottom, 20.0)
            }
            .fixedSize(horizontal: false, vertical: true)
            .background(Color.gray.opacity(0.5))
            .cornerRadius(28)
            .clipped()
            .padding([.leading, .trailing], 5.0)
            .position(x: geometry.size.width/2, y: geometry.size.height/2)
            .frame(minWidth: 250, maxWidth: 350)
            //.allowsHitTesting(false)
        }
    }
    
    func showAlert() {
        let swiftUIView = alertContent()
        alertHostingController = UIHostingController(rootView: AnyView(swiftUIView))
        alertHostingController?.view.backgroundColor = .clear
        alertHostingController?.view.frame = CGRect(x: 0, y: UIScreen.main.bounds.height, width: UIScreen.main.bounds.width - 44.0, height: 0)

        if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
           let window = windowScene.windows.first, let hostingController = alertHostingController {
            window.addSubview(hostingController.view)

            hostingController.view.center.x = window.center.x
            hostingController.view.center.y = window.center.y
        }
    }
    
    func dismissAlert() {
        UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
            alertHostingController?.view.frame = CGRect(x: 0, y: UIScreen.main.bounds.height, width: UIScreen.main.bounds.width, height: 0)
            alertHostingController?.view.alpha = 0
        }

        Task {
            await MainActor.run {
                alertHostingController?.view.removeFromSuperview()
                alertHostingController = nil
                alertViewModel.showAlert = false
            }
        }
    }
}

// MARK: OVERLAY CODE

struct OverlayView<OverlayContent: View>: ViewModifier {
    @Binding var isPresented: Bool

    var modalContent: OverlayContent

    func body(content: Content) -> some View {
        GeometryReader { _ in
            ZStack {
                content
                VStack {
                    if isPresented {
                        modalContent
                    } else {
                        Spacer()
                    }
                }
            }
        }
    }
}

extension View {
    @ViewBuilder
    func overlayModal<ModalContent: View>(isPresented: Binding<Bool>, @ViewBuilder modalContent: @escaping () -> ModalContent) -> some View {
        modifier(OverlayView(isPresented: isPresented, modalContent: modalContent()))
    }
}

Any idea why my tap gestures aren't working? Any other solution to present a custom alert on top of all views would be welcomed.


Solution

  • You're setting the height of the view to 0:

            alertHostingController?.view.frame = CGRect(x: 0, y: UIScreen.main.bounds.height, width: UIScreen.main.bounds.width - 44.0, height: 0)
    

    Although it's still displaying, because it's not clipping to the bounds, it's not registering any user input. Instead, you likely want to pin the edges of the view to its superview.

    func showAlert() {
        let swiftUIView = alertContent()
        let alertHostingController = UIHostingController(rootView: AnyView(swiftUIView))
        alertHostingController.view.backgroundColor = .clear
    
        if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
           let window = windowScene.windows.first {
            window.addSubview(alertHostingController.view)
            self.alertHostingController = alertHostingController // Store the controller if needed
    
            // Pin the view to the edges of the window
            alertHostingController.view.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                alertHostingController.view.topAnchor.constraint(equalTo: window.topAnchor),
                alertHostingController.view.bottomAnchor.constraint(equalTo: window.bottomAnchor),
                alertHostingController.view.leadingAnchor.constraint(equalTo: window.leadingAnchor),
                alertHostingController.view.trailingAnchor.constraint(equalTo: window.trailingAnchor)
            ])
        }
    }