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.
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)
])
}
}