Last active
June 11, 2025 11:10
-
-
Save michael94ellis/2b2fe959bf2416a7a61140602ebed0cf to your computer and use it in GitHub Desktop.
A simple way to show a toast in SwiftUI apps over all other view activity.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Inspired by Federico Zanetello | |
// https://www.fivestars.blog/articles/swiftui-windows/ | |
import UIKit | |
import SwiftUI | |
struct ToastView: View { | |
let message: String | |
/// Total duration that the toast will be visible on screen | |
let duration: TimeInterval | |
/// Duration of the movement in and out | |
let animationDuration: TimeInterval | |
/// Opacity control | |
@State private var isVisible = false | |
/// Start position | |
@State private var offsetY: CGFloat = 50 | |
var body: some View { | |
VStack { | |
Spacer() | |
Text(message) | |
.padding() | |
.background(Color.red.opacity(0.8)) | |
.foregroundColor(.white) | |
.cornerRadius(8) | |
.opacity(isVisible ? 1 : 0) | |
.offset(y: offsetY) | |
.onAppear { | |
withAnimation(.easeInOut(duration: animationDuration)) { | |
offsetY = -50 | |
isVisible = true | |
} | |
DispatchQueue.main.asyncAfter(deadline: .now() + (duration - animationDuration)) { | |
withAnimation(.easeInOut(duration: animationDuration)) { | |
offsetY = 50 // Moves back down | |
isVisible = false | |
} | |
} | |
} | |
} | |
} | |
} | |
final class Toaster { | |
static let shared = Toaster() | |
private var toastWindow: UIWindow? | |
var activeWindow: UIWindow? { | |
UIApplication.shared.connectedScenes | |
.compactMap { $0 as? UIWindowScene } | |
.flatMap { $0.windows } | |
.first { $0.isKeyWindow } | |
} | |
func showToast(message: String, duration: TimeInterval = 2.0) { | |
if let window = activeWindow, | |
let scene = window.windowScene { | |
if toastWindow == nil { | |
let toastWindow = PassThroughWindow(windowScene: scene) | |
toastWindow.windowLevel = .alert + 1 | |
toastWindow.backgroundColor = UIColor.clear | |
self.toastWindow = toastWindow | |
} | |
let toastView = ToastView(message: message, | |
duration: duration, | |
animationDuration: 0.3) | |
let toastViewController = UIHostingController(rootView: toastView) | |
toastViewController.view.backgroundColor = UIColor.clear | |
toastWindow?.rootViewController = toastViewController | |
toastWindow?.isHidden = false | |
DispatchQueue.main.asyncAfter(deadline: .now() + duration) { | |
self.toastWindow?.isHidden = true | |
} | |
} | |
} | |
} | |
class PassThroughWindow: UIWindow { | |
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { | |
// Get view from superclass. | |
guard let hitView = super.hitTest(point, with: event) else { return nil } | |
// If the returned view is the `UIHostingController`'s view, ignore. | |
return rootViewController?.view == hitView ? nil : hitView | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A Demo of this code