SwiftUI中如何在导航栏上方展示不阻断交互的Toast/Banner(类似UIKit中给UIWindow添加子视图的实现方式)
嘿,我太懂这种从UIKit转SwiftUI的别扭感了!你要的那种能盖在导航栏上方、还不耽误用户点下面内容的Toast,用fullScreenCover肯定不行——那玩意儿会直接把整个页面锁住,完全不符合需求。我给你两种靠谱的实现方式,按需选就行:
方法一:纯SwiftUI原生实现(推荐)
这种方式完全贴合SwiftUI的状态管理逻辑,不需要桥接UIKit,代码也更清爽:
- 先搞一个全局的Toast管理器,用来控制Toast的显示状态和内容:
import SwiftUI class ToastManager: ObservableObject { @Published var isShowing = false @Published var message = "" private var timer: Timer? // 对外暴露的显示方法,支持自定义时长 func showToast(message: String, duration: TimeInterval = 3) { self.message = message self.isShowing = true // 先清除之前的定时器,避免重复触发 timer?.invalidate() timer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { [weak self] _ in self?.isShowing = false } } }
- 在App的根视图里全局挂载Toast:
把整个App的根内容包在ZStack里,让Toast始终处于最上层,同时设置allowsHitTesting(false)让下层内容可以正常交互:
@main struct YourAwesomeApp: App { // 用StateObject创建全局的Toast管理器 @StateObject private var toastManager = ToastManager() var body: some Scene { WindowGroup { ZStack { // 这里放你的根视图,比如TabView、NavigationStack之类的 ContentView() .environmentObject(toastManager) // 把管理器注入到整个视图树 // 只有在需要显示时才渲染Toast if toastManager.isShowing { ToastView(message: toastManager.message) .transition(.opacity.combined(with: .move(edge: .top))) // 加个过渡动画 .animation(.easeInOut(duration: 0.3), value: toastManager.isShowing) .allowsHitTesting(false) // 关键:让下层内容可以被点击 } } } } }
- 自定义Toast的样式:
根据你的需求调整外观,比如背景色、字体、位置:
struct ToastView: View { let message: String var body: some View { Text(message) .font(.system(size: 14)) .padding(.vertical, 12) .padding(.horizontal, 16) .background(Color.black.opacity(0.8)) .foregroundColor(.white) .cornerRadius(8) .padding(.top, 40) // 避开状态栏,也可以用safeAreaInsets适配 .alignmentGuide(.top) { $0[.top] } } }
- 在任意SwiftUI页面调用:
通过EnvironmentObject拿到管理器,直接调用showToast就行:
struct ContentView: View { @EnvironmentObject private var toastManager: ToastManager var body: some View { NavigationStack { Button("触发Toast") { toastManager.showToast(message: "操作成功啦!") } .navigationTitle("测试页面") } } }
方法二:复用UIKit的老思路(适合习惯UIKit的同学)
如果你更熟悉UIKit里给UIWindow加子视图的方式,直接把这套逻辑搬过来就行,完全兼容SwiftUI:
- 创建UIKit的Toast管理类:
import UIKit class UIKitToastManager { static let shared = UIKitToastManager() private var toastView: UIView? func showToast(message: String, duration: TimeInterval = 3) { // 先移除之前的Toast,避免重复显示 toastView?.removeFromSuperview() // 创建Toast标签 let toastLabel = UILabel() toastLabel.text = message toastLabel.textColor = .white toastLabel.backgroundColor = .black.withAlphaComponent(0.8) toastLabel.textAlignment = .center toastLabel.layer.cornerRadius = 8 toastLabel.clipsToBounds = true toastLabel.numberOfLines = 0 // 给标签加内边距 toastLabel.frame = toastLabel.frame.inset(by: UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)) // 获取当前活跃的主窗口(iOS13+适配多场景) guard let window = UIApplication.shared.connectedScenes .filter({$0.activationState == .foregroundActive}) .compactMap({$0 as? UIWindowScene}) .first?.windows .first(where: {$0.isKeyWindow}) else { return } // 把Toast加到窗口上 window.addSubview(toastLabel) toastView = toastLabel // 设置约束,让Toast在顶部居中 toastLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ toastLabel.centerXAnchor.constraint(equalTo: window.centerXAnchor), toastLabel.topAnchor.constraint(equalTo: window.safeAreaLayoutGuide.topAnchor, constant: 16) ]) // 自动隐藏Toast,加个淡出动画 DispatchQueue.main.asyncAfter(deadline: .now() + duration) { UIView.animate(withDuration: 0.3) { toastLabel.alpha = 0 } completion: { _ in toastLabel.removeFromSuperview() self.toastView = nil } } // 如果不需要Toast响应点击,设置这个让下层可以交互 toastLabel.isUserInteractionEnabled = false } }
- 在SwiftUI页面直接调用:
不需要任何状态注入,直接调用单例的方法就行:
struct ContentView: View { var body: some View { NavigationStack { Button("显示UIKit Toast") { UIKitToastManager.shared.showToast(message: "这是UIKit实现的Toast") } .navigationTitle("测试页面") } } }
一些小提醒
- 如果你的Toast需要支持点击(比如点击关闭),第一种SwiftUI方法不要给整个
ToastView加allowsHitTesting(false),可以只给背景层加:ToastView(message: toastManager.message) .transition(...) .animation(...) .background(Color.clear.allowsHitTesting(false)) - 第二种UIKit方法里,要是需要Toast能响应点击,把
toastLabel.isUserInteractionEnabled = false去掉就行,同时可以给标签加手势识别器。
内容来源于stack exchange
相关产品推荐
相关产品推荐

