You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

切换ViewController时如何移除其调用的Toast视图?

解决切换控制器时Toast不消失的问题

核心问题分析

当前实现里,Toast的消失依赖DispatchQueue.main.asyncAfter的延迟任务,但切换控制器时,原控制器视图虽不在前台,延迟任务仍会继续执行,且Toast未被主动移除,导致返回后残留。

解决方案步骤

给控制器添加Toast跟踪管理机制,结合生命周期方法主动清理:

  1. 添加Toast跟踪属性:在UIViewController扩展中存储当前显示的Toast及其对应的延迟任务,方便统一管理。
  2. 关联任务与Toast:创建Toast时,将延迟任务和Toast实例绑定存储,确保可随时取消任务、移除Toast。
  3. 生命周期触发清理:在控制器即将消失时(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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 20:50:24