exyte / PopupView

Toasts and popups library written with SwiftUI
MIT License
3.38k stars 258 forks source link

Toast is hidden by Popover #204

Open saff1x opened 1 month ago

saff1x commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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