切换ViewController时如何移除其调用的Toast视图?
解决切换控制器时Toast不消失的问题
核心问题分析
当前实现里,Toast的消失依赖DispatchQueue.main.asyncAfter的延迟任务,但切换控制器时,原控制器视图虽不在前台,延迟任务仍会继续执行,且Toast未被主动移除,导致返回后残留。
解决方案步骤
给控制器添加Toast跟踪管理机制,结合生命周期方法主动清理:
- 添加Toast跟踪属性:在
UIViewController扩展中存储当前显示的Toast及其对应的延迟任务,方便统一管理。 - 关联任务与Toast:创建Toast时,将延迟任务和Toast实例绑定存储,确保可随时取消任务、移除Toast。
- 生命周期触发清理:在控制器即将消失时(
viewWillDisappear),遍历所有未完成的Toast,立即停止动画、移除视图并取消延迟任务。
完整实现代码
// 补充必要的枚举与类型定义 enum ToastPosition { case top case bottom } typealias Handler = () -> Void // 简化ToastView定义(适配示例编译) class ToastView: UIView { init(viewModel: Any, frame: CGRect) { super.init(frame: frame) backgroundColor = .darkGray.withAlphaComponent(0.8) // 这里替换为你的ToastView初始化逻辑 } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension UIViewController { // 用关联对象存储活跃Toast和对应延迟任务 private struct AssociatedKeys { static var activeToasts = "activeToasts" } private var activeToasts: [(toast: ToastView, task: DispatchWorkItem)] { get { return objc_getAssociatedObject(self, &AssociatedKeys.activeToasts) as? [(toast: ToastView, task: DispatchWorkItem)] ?? [] } set { objc_setAssociatedObject(self, &AssociatedKeys.activeToasts, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } func showInfoToastView(text: String, image: UIImage?, duration: Double, position: ToastPosition) { // 替换为你的viewModel创建逻辑 let viewModel = "" let toast = ToastView(viewModel: viewModel, frame: .zero) view.addSubview(toast) // 创建延迟退出任务并存储 let exitTask = DispatchWorkItem { [weak self, weak toast] in guard let self = self, let toast = toast else { return } self.slideOutToast(toast: toast, position: position) } activeToasts.append((toast: toast, task: exitTask)) slideInToast(toast: toast, position: position, duration: duration, exitTask: exitTask) } func showActionToastView(text: String, image: UIImage?, duration: Double, position: ToastPosition, action: Handler?) { // 替换为你的viewModel创建逻辑 let viewModel = "" let toast = ToastView(viewModel: viewModel, frame: .zero) view.addSubview(toast) // 创建延迟退出任务并存储 let exitTask = DispatchWorkItem { [weak self, weak toast] in guard let self = self, let toast = toast else { return } self.slideOutToast(toast: toast, position: position) } activeToasts.append((toast: toast, task: exitTask)) slideInToast(toast: toast, position: position, duration: duration, exitTask: exitTask) } private func slideInToast(toast: ToastView, position: ToastPosition, duration: Double, exitTask: DispatchWorkItem) { let width = UIScreen.main.bounds.width / 1.2 let barHeight = topbarHeight switch position { case .top: toast.frame = CGRect(x: (UIScreen.main.bounds.width - width)/2, y: -70 - barHeight, width: width, height: 60) UIView.animate(withDuration: 0.5) { toast.frame = CGRect(x: (UIScreen.main.bounds.width - width)/2, y: 70 - barHeight, width: width, height: 60) } done: { done in if done { DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: exitTask) } } case .bottom: toast.frame = CGRect(x: (view.frame.width - width)/2, y: UIScreen.main.bounds.height, width: width, height: 60) UIView.animate(withDuration: 0.5) { toast.frame = CGRect(x: (self.view.frame.width - width)/2, y: UIScreen.main.bounds.height - 200 - self.tabBarHeight, width: width, height: 60) } done: { done in if done { DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: exitTask) } } } } private func slideOutToast(toast: ToastView, position: ToastPosition) { let width = UIScreen.main.bounds.width / 1.2 let barHeight = topbarHeight switch position { case .top: UIView.animate(withDuration: 0.5) { toast.frame = CGRect(x: (UIScreen.main.bounds.width - width)/2, y: -70 - barHeight, width: width, height: 60) } done: { [weak self, weak toast] finished in if finished { toast?.removeFromSuperview() self?.activeToasts.removeAll { $0.toast === toast } } } case .bottom: UIView.animate(withDuration: 0.5) { toast.frame = CGRect(x: (self.view.frame.width - width)/2, y: UIScreen.main.bounds.height, width: width, height: 60) } done: { [weak self, weak toast] finished in if finished { toast?.removeFromSuperview() self?.activeToasts.removeAll { $0.toast === toast } } } } } // 控制器消失时清理所有Toast open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) for (toast, task) in activeToasts { task.cancel() toast.removeFromSuperview() } activeToasts.removeAll() } // 补充原有的导航栏、tabBar高度计算 private var topbarHeight: CGFloat { let statusBarHeight = UIApplication.shared.windows.first?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0 let navBarHeight = navigationController?.navigationBar.frame.height ?? 0 return statusBarHeight + navBarHeight } private var tabBarHeight: CGFloat { return tabBarController?.tabBar.frame.height ?? 0 } }
关键说明
- 关联对象存储:通过
objc_getAssociatedObject给扩展添加存储属性,实现对活跃Toast和任务的跟踪。 - 延迟任务管理:用
DispatchWorkItem封装退出逻辑,方便在控制器消失时直接取消任务,避免无效执行。 - 生命周期清理:在
viewWillDisappear中统一处理所有Toast,确保控制器切换时无残留视图。
内容的提问来源于stack exchange,提问作者Geheiligt
相关产品推荐
相关产品推荐

