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

Swift中向Window添加子视图后按钮点击无响应问题

自定义Toast按钮点击无响应问题解决

问题描述

给Window添加自定义Toast视图,包含进度条和关闭按钮,但按钮点击无响应,无法触发点击事件。已设置视图和按钮的isUserInteractionEnabled = true,且确认toastView处于视图层级最上层,问题仍存在。

相关代码

ToastView实现

class iFractalToastView: UIView {
   
    private let tituloLabel = {
        let label = iFractalTituloLabel(title: "STATUS", color: .PGF)
        label.textAlignment = .center
        label.font = UIFont(name: "MuseoSans-700", size: 14)
        return label
    }()
    
    private let messageLabel = {
        let label = iFractalBodyLabel(title: "", isBold: false)
        label.textAlignment = .center
        return label
    }()
    
    let progressBar = UIProgressView(progressViewStyle: .default)
    let dismissButton = iFractalAlertButton(title: "OK")
    
    var dismissAction: (() -> Void)?
    
    private var message = String()
    private var button = Bool()
    
    init(message: String, button: Bool) {
        super.init(frame: .zero)
        self.isUserInteractionEnabled = true
        dismissButton.isUserInteractionEnabled = true
        dismissButton.addTarget(self, action: #selector(dismissButtonTapped), for: .touchUpInside)
        self.message = message
        self.button = button
        self.translatesAutoresizingMaskIntoConstraints = false
        iniciateSubViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setProgress(_ progress: Float) {
        progressBar.setProgress(progress, animated: true)
    }
    
    @objc func dismissButtonTapped() {
        print("ENTROUU dismissButtonTapped")
        dismissAction?()
    }
   
    func imageWithColor(_ color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
        let format = UIGraphicsImageRendererFormat()
        format.scale = 1
        let image =  UIGraphicsImageRenderer(size: size, format: format).image { rendererContext in
            color.setFill()
            rendererContext.fill(CGRect(origin: .zero, size: size))
        }
        return image
    }
    
    private func iniciateSubViews() {
        addSubViews()
        constraintsSubViews()
        configureSubviews()
    }
    
    private func addSubViews() {
        addSubview(tituloLabel)
        addSubview(messageLabel)
        addSubview(progressBar)
        if button == true {
            addSubview(dismissButton)
        }
    }
    
    private func constraintsSubViews() {
        tituloLabel.translatesAutoresizingMaskIntoConstraints = false
        messageLabel.translatesAutoresizingMaskIntoConstraints = false
        progressBar.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            tituloLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 10),
            tituloLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10),
            tituloLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10),
            
            messageLabel.topAnchor.constraint(equalTo: tituloLabel.bottomAnchor, constant: 10),
            messageLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10),
            messageLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10),
            
            progressBar.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: 10),
            progressBar.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0),
            progressBar.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0),
            progressBar.heightAnchor.constraint(equalToConstant: 4)
        ])
        
        if button == true {
            dismissButton.translatesAutoresizingMaskIntoConstraints = false
            
            NSLayoutConstraint.activate([
                dismissButton.topAnchor.constraint(equalTo: progressBar.bottomAnchor, constant: 10),
                dismissButton.centerXAnchor.constraint(equalTo: self.centerXAnchor),
                dismissButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -10),
                dismissButton.widthAnchor.constraint(equalToConstant: 100)
            ])
        }
        else {
            NSLayoutConstraint.activate([
                progressBar.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -10)
            ])
        }
    }
    
    private func configureSubviews() {
        backgroundColor = .TD90
        layer.cornerRadius = 10
        layer.borderColor = UIColor.PGF.cgColor
        layer.borderWidth = 1.0
        clipsToBounds = true
        
        // Configure the message label
        messageLabel.text = message
        messageLabel.textAlignment = .center
        messageLabel.numberOfLines = 0
        
        // Configure the progress bar
        progressBar.progress = 0.0
        progressBar.progressImage = imageWithColor(.PGF)
        
        //dismissButton.setTitle("OK", for: .normal)
        //dismissButton.setTitleColor(.white, for: .normal)
        dismissButton.clipsToBounds = true
    }
}

UIViewController中的showToast函数

func showToast(message: String, duration: TimeInterval = 60.0, button: Bool = false) {
        let toastView = iFractalToastView(message: message, button: button)
       
        // Configure toast view appearance
        toastView.alpha = 0.0
        toastView.translatesAutoresizingMaskIntoConstraints = false
        if button == true {
            toastView.dismissAction = {
                UIView.animate(withDuration: 0.5, animations: {
                    toastView.alpha = 0.0
                }) { _ in
                    toastView.removeFromSuperview()
                }
            }
        }
        
        // Add the toast view to the view controller's view
        let window = UIApplication.shared.keyWindow!
        window.addSubview(toastView)
        
        // Set up constraints
        if button == false {
            NSLayoutConstraint.activate([
                toastView.leadingAnchor.constraint(equalTo: window.leadingAnchor, constant: 10),
                toastView.trailingAnchor.constraint(equalTo: window.trailingAnchor, constant: -10),
                toastView.topAnchor.constraint(equalTo: window.topAnchor, constant: 41)
            ])
        }
        else {
            NSLayoutConstraint.activate([
                toastView.leadingAnchor.constraint(equalTo: window.leadingAnchor, constant: 10),
                toastView.trailingAnchor.constraint(equalTo: window.trailingAnchor, constant: -10),
                toastView.centerXAnchor.constraint(equalTo: window.centerXAnchor),
                toastView.centerYAnchor.constraint(equalTo: window.centerYAnchor)
            ])
        }
       
        window.bringSubviewToFront(toastView)
       
        // Animate the toast view appearance
        UIView.animate(withDuration: 0.5, animations: {
            toastView.alpha = 1.0
        }) { _ in
            // Animate the progress bar
            let progress = Float(duration)
            toastView.setProgress(progress)
            
            // Animate the toast view disappearance
            UIView.animate(withDuration: 0.5, delay: duration, options: .curveEaseOut, animations: {
                toastView.alpha = 0.0
            }) { _ in
                toastView.removeFromSuperview()
            }
        }
    }

问题原因

  1. 动画禁用用户交互:UIView默认动画会自动禁用视图的用户交互,你的Toast在出现、进度条动画、消失三个阶段都使用了UIView.animate且未添加.allowUserInteraction选项,导致按钮在整个Toast生命周期内无法响应点击。
  2. 进度条参数错误:UIProgressView的progress属性范围是0到1,你传入了Float(duration)(如60),会导致进度条直接跳满,达不到预期动画效果,但这不是按钮点击的直接原因。
  3. keyWindow适配问题:UIApplication.shared.keyWindow在iOS 13+已被废弃,可能导致Toast无法正确添加到当前活跃窗口。

解决方案

1. 给所有动画添加.allowUserInteraction选项

修改showToast函数中的动画部分,确保每个动画都允许用户交互:

// 出现动画
UIView.animate(withDuration: 0.5, delay: 0, options: [.allowUserInteraction], animations: {
    toastView.alpha = 1.0
}) { _ in
    // 进度条动画(修正进度值为1.0,时长和Toast保持一致)
    UIView.animate(withDuration: duration, delay: 0, options: [.allowUserInteraction], animations: {
        toastView.progressBar.setProgress(1.0, animated: true)
    })
    
    // 消失动画
    UIView.animate(withDuration: 0.5, delay: duration, options: [.curveEaseOut, .allowUserInteraction], animations: {
        toastView.alpha = 0.0
    }) { _ in
        toastView.removeFromSuperview()
    }
}

2. 替换keyWindow的获取方式(适配iOS 13+)

替换原来的window获取代码:

guard let window = UIApplication.shared.connectedScenes
        .filter({ $0.activationState == .foregroundActive })
        .compactMap({ $0 as? UIWindowScene })
        .first?
        .windows
        .first(where: { $0.isKeyWindow }) else {
    return
}

3. 检查自定义按钮iFractalAlertButton

确保自定义按钮的isUserInteractionEnabled默认开启,且没有重写hitTest或pointInside方法拦截点击事件。

验证修改

修改后运行代码,按钮应该能正常响应点击,触发dismissButtonTapped方法,同时进度条会按预期从0到1动画显示。

内容的提问来源于stack exchange,提问作者Lucca Germano

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 12:25:54