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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 06:27:09