Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Toast is hidden by Popover #204

Open
saff1x opened this issue Jul 27, 2024 · 3 comments
Open

Toast is hidden by Popover #204

saff1x opened this issue Jul 27, 2024 · 3 comments

Comments

@saff1x
Copy link

saff1x commented Jul 27, 2024

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.

@f3dm76
Copy link
Collaborator

f3dm76 commented Jul 29, 2024

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

@saff1x
Copy link
Author

saff1x commented Jul 29, 2024

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
    }
}

@Jerland2
Copy link

Jerland2 commented Oct 5, 2024

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants