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() } } }
问题原因
- 动画禁用用户交互:UIView默认动画会自动禁用视图的用户交互,你的Toast在出现、进度条动画、消失三个阶段都使用了UIView.animate且未添加
.allowUserInteraction选项,导致按钮在整个Toast生命周期内无法响应点击。 - 进度条参数错误:UIProgressView的progress属性范围是0到1,你传入了
Float(duration)(如60),会导致进度条直接跳满,达不到预期动画效果,但这不是按钮点击的直接原因。 - 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
相关产品推荐
相关产品推荐

