Swift中#selector无法引用全局函数,如何实现Toast点击消失?
解决全局Toast点击消失的Selector问题
方案一:将Toast逻辑封装到NSObject子类中
因为@objc只能修饰类的成员方法,且类需要继承自NSObject才能让方法暴露给Objective-C(#selector依赖这个机制)。我们可以把Toast的创建、显示、点击处理都封装到一个单例类里,这样就能合法使用@objc修饰点击方法了。
示例代码:
import UIKit class ToastManager: NSObject { // 单例,全局唯一实例 static let shared = ToastManager() // 记录当前显示的Toast,避免重复叠加 private var currentToast: UIView? func showToast(message: String, in containerView: UIView) { // 先移除已存在的Toast currentToast?.removeFromSuperview() // 创建Toast容器视图 let toastView = UIView() toastView.backgroundColor = UIColor.black.withAlphaComponent(0.7) toastView.layer.cornerRadius = 8 toastView.translatesAutoresizingMaskIntoConstraints = false // 创建全屏按钮,接收点击事件(点击Toast任意位置都能触发消失) let tapButton = UIButton(type: .custom) tapButton.translatesAutoresizingMaskIntoConstraints = false tapButton.addTarget(self, action: #selector(dismissToast), for: .touchUpInside) toastView.addSubview(tapButton) // 添加文字标签 let messageLabel = UILabel() messageLabel.text = message messageLabel.textColor = .white messageLabel.textAlignment = .center messageLabel.numberOfLines = 0 messageLabel.translatesAutoresizingMaskIntoConstraints = false toastView.addSubview(messageLabel) // 添加Toast到容器视图 containerView.addSubview(toastView) currentToast = toastView // 设置AutoLayout约束(适配不同屏幕) NSLayoutConstraint.activate([ toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), toastView.bottomAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.bottomAnchor, constant: -20), toastView.leadingAnchor.constraint(greaterThanOrEqualTo: containerView.leadingAnchor, constant: 20), toastView.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor, constant: -20), tapButton.topAnchor.constraint(equalTo: toastView.topAnchor), tapButton.bottomAnchor.constraint(equalTo: toastView.bottomAnchor), tapButton.leadingAnchor.constraint(equalTo: toastView.leadingAnchor), tapButton.trailingAnchor.constraint(equalTo: toastView.trailingAnchor), messageLabel.topAnchor.constraint(equalTo: toastView.topAnchor, constant: 12), messageLabel.bottomAnchor.constraint(equalTo: toastView.bottomAnchor, constant: -12), messageLabel.leadingAnchor.constraint(equalTo: toastView.leadingAnchor, constant: 16), messageLabel.trailingAnchor.constraint(equalTo: toastView.trailingAnchor, constant: -16) ]) // 3秒后自动消失 Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { [weak self] _ in self?.dismissToast() } } // 点击Toast或自动触发的消失方法 @objc private func dismissToast() { guard let toast = currentToast else { return } UIView.animate(withDuration: 0.3) { toast.alpha = 0 } completion: { _ in toast.removeFromSuperview() self.currentToast = nil } } }
调用方式:
// 在任意ViewController中调用 ToastManager.shared.showToast(message: "操作成功", in: self.view)
方案二:用闭包替代Target-Action机制
如果不想封装类,可以给UIButton添加一个支持闭包回调的扩展,绕开#selector的限制,直接在创建按钮时传入点击后的处理逻辑。
示例代码:
首先给UIButton写扩展:
import UIKit extension UIButton { // 关联对象存储闭包 private struct AssociatedKeys { static var tapClosure = "tapClosure" } var onTap: (() -> Void)? { get { return objc_getAssociatedObject(self, &AssociatedKeys.tapClosure) as? (() -> Void) } set { objc_setAssociatedObject(self, &AssociatedKeys.tapClosure, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) // 添加点击事件监听 addTarget(self, action: #selector(handleTap), for: .touchUpInside) } } @objc private func handleTap() { onTap?() } }
然后修改全局的showToast函数:
import UIKit func showToast(message: String, in containerView: UIView) { // 移除已存在的Toast(通过tag标记识别) if let existingToast = containerView.subviews.first(where: { $0.tag == 1001 }) { existingToast.removeFromSuperview() } // 创建Toast视图 let toastView = UIView() toastView.backgroundColor = UIColor.black.withAlphaComponent(0.7) toastView.layer.cornerRadius = 8 toastView.tag = 1001 // 标记Toast,方便后续移除 toastView.translatesAutoresizingMaskIntoConstraints = false // 创建点击按钮 let tapButton = UIButton(type: .custom) tapButton.translatesAutoresizingMaskIntoConstraints = false // 直接设置点击闭包 tapButton.onTap = { UIView.animate(withDuration: 0.3) { toastView.alpha = 0 } completion: { _ in toastView.removeFromSuperview() } } toastView.addSubview(tapButton) // 添加文字标签 let messageLabel = UILabel() messageLabel.text = message messageLabel.textColor = .white messageLabel.textAlignment = .center messageLabel.numberOfLines = 0 messageLabel.translatesAutoresizingMaskIntoConstraints = false toastView.addSubview(messageLabel) containerView.addSubview(toastView) // 设置约束 NSLayoutConstraint.activate([ toastView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), toastView.bottomAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.bottomAnchor, constant: -20), toastView.leadingAnchor.constraint(greaterThanOrEqualTo: containerView.leadingAnchor, constant: 20), toastView.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor, constant: -20), tapButton.topAnchor.constraint(equalTo: toastView.topAnchor), tapButton.bottomAnchor.constraint(equalTo: toastView.bottomAnchor), tapButton.leadingAnchor.constraint(equalTo: toastView.leadingAnchor), tapButton.trailingAnchor.constraint(equalTo: toastView.trailingAnchor), messageLabel.topAnchor.constraint(equalTo: toastView.topAnchor, constant: 12), messageLabel.bottomAnchor.constraint(equalTo: toastView.bottomAnchor, constant: -12), messageLabel.leadingAnchor.constraint(equalTo: toastView.leadingAnchor, constant: 16), messageLabel.trailingAnchor.constraint(equalTo: toastView.trailingAnchor, constant: -16) ]) // 自动消失定时器 Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in UIView.animate(withDuration: 0.3) { toastView.alpha = 0 } completion: { _ in toastView.removeFromSuperview() } } }
调用方式:
showToast(message: "操作成功", in: self.view)
问题根源解释
Swift全局函数无法被@objc修饰,而#selector要求方法必须能被Objective-C runtime识别(只有NSObject子类的成员方法加@objc才能满足),所以直接给全局函数加@objc会报错,用#selector引用全局函数也会失败。上面两种方案分别从封装类让方法符合Objective-C要求和绕开selector用闭包处理事件两个角度解决了问题。
内容的提问来源于stack exchange,提问作者Fahim Parkar
相关产品推荐
相关产品推荐

