PopupView icon indicating copy to clipboard operation
PopupView copied to clipboard

Toast is hidden by Popover

Open saff1x opened this issue 1 year ago • 3 comments

Hi, I want to use a toast to display error messages in my app (The toast should be at the bottom of the screen). However i noticed that when using a native .popover screen, the toast is below the popover and can't be seen. I tried using the .isOpaque(true) modifier, however that just closes the popover, which is not what I want. Is there any way to fix this? Thanks in advance.

saff1x avatar Jul 27 '24 12:07 saff1x

Hey @saff1x, opaque popup is just a fullscreen cover, I can't control what it covers. Apple also doesn't give me any way to place any custom views above the system views like sheets, popovers and navbar, so no control there either. If you know of a way to bypass that restriction, you are most welcome to commit a PR. Have a nice day

f3dm76 avatar Jul 29 '24 03:07 f3dm76

I found a workaround based on this article: https://www.fivestars.blog/articles/swiftui-windows/

Not really sure what its doing and its probably not the best solution but it works for me. Hope it helps

import UIKit
import SwiftUI
import ExytePopupView

class SnackbarManager: ObservableObject{
    @Published var showSnackbar: Bool = false
    @Published var snackbarMessage: String = ""
    
    static let shared = SnackbarManager()
    
    private init() {}
    
    func showSnackbar(message: String) {
        snackbarMessage = message
        showSnackbar = true
    }
    
    func dismissSnackbar() {
        showSnackbar = false
    }
    
}

struct SnackbarViewModifier: ViewModifier {
    @ObservedObject var snackbarManager = SnackbarManager.shared
    
    func body(content: Content) -> some View {
        content
            .popup(
                isPresented: Binding(
                    get: { snackbarManager.showSnackbar },
                    set: { value in
                        snackbarManager.showSnackbar = value
                    }
                ),
                view: {
                    Snackbar(message: snackbarManager.snackbarMessage)
                },
                customize: {
                    $0
                        .type(.toast)
                        .position(.bottom)
                        .autohideIn(4)
                        .dragToDismiss(true)
                        .useKeyboardSafeArea(true)
                }
            )
    }
}


final class SceneDelegate: NSObject, UIWindowSceneDelegate {
    
    var secondaryWindow: UIWindow?
    
    func scene(_ scene: UIScene,
               willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            setupSecondaryOverlayWindow(in: windowScene)
        }
    }
    
    func setupSecondaryOverlayWindow(in scene: UIWindowScene) {
        let secondaryViewController = UIHostingController(
            rootView:
                EmptyView()
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .modifier(SnackbarViewModifier())
        )
        secondaryViewController.view.backgroundColor = .clear
        let secondaryWindow = PassThroughWindow(windowScene: scene)
        secondaryWindow.rootViewController = secondaryViewController
        secondaryWindow.isHidden = false
        self.secondaryWindow = secondaryWindow
    }
}

class PassThroughWindow: UIWindow {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard let hitView = super.hitTest(point, with: event) else { return nil }
        return rootViewController?.view == hitView ? nil : hitView
    }
}

And then added this in the iosApp file:

import SwiftUI

@main
struct iosApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { return true }
    
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
        if connectingSceneSession.role == .windowApplication {
            configuration.delegateClass = SceneDelegate.self
        }
        return configuration
    }
}

saff1x avatar Jul 29 '24 15:07 saff1x

@saff1x are you able to use buttons within your toast? I am using the same approach, however I am unable to get Buttons. I think the PassThroughWindow is intercepting the touches. Wondering how you solved this problem.

I do get the PassThroughWindow to successfully register taps on the screen both in the toast as well as other parts of the view. But the taps do not make it through to the toast.

Jerland2 avatar Oct 05 '24 23:10 Jerland2

Hey guys, in version 4.0.0, I added .window display type for a popup, does this by any chance solve your issue? have a good day!

f3dm76 avatar Feb 12 '25 10:02 f3dm76