UIView.animateKeyFrames致UIButton点击失效:视图层级还是NSLayoutConstraint问题?
问题
我写了一个ToastView类,用来创建带按钮、标签和图片的提示视图,要让它作为子视图在控制器里像通知栏一样上下滑动。在layoutOnController()方法里,我通过父视图的边距用NSLayoutConstraint把视图放在可视区域下方,再用关键帧动画让它向上平移。
问题是:执行向上动画后,按钮完全无法交互;但如果只调用layoutOnController(),按钮能正常点击触发@objc方法。
我猜问题出在ToastViewController添加子视图的逻辑,或者是NSLayoutConstraint和UIView.AnimateKeyFrames的兼容问题。试过用修改布局常量(self.bottom.constant = 50)代替transform平移,但视图直接不显示,卡了很久,求建议。
ToastView代码
import UIKit import Alamofire import CoreGraphics class ToastView: UIView { let toastSuperviewMargins: UILayoutGuide let toastRenderType: String var top = NSLayoutConstraint() var bottom = NSLayoutConstraint() var width = NSLayoutConstraint() var height = NSLayoutConstraint() var trailing = NSLayoutConstraint() var leading = NSLayoutConstraint() private let label: UILabel = { let label = UILabel() label.textAlignment = .left label.font = .systemFont(ofSize: 15) label.textColor = UIColor.white label.numberOfLines = 0 label.translatesAutoresizingMaskIntoConstraints = false return label }() private let dismissButton: UIButton = { let dismissButton = UIButton(type: .custom) dismissButton.isUserInteractionEnabled = true dismissButton.setTitleColor(UIColor.white, for: .normal) dismissButton.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .semibold) dismissButton.translatesAutoresizingMaskIntoConstraints = false return dismissButton }() private let imageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.clipsToBounds = true imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() init(toastRenderType: String, toastController: UIViewController, frame: CGRect) { self.toastSuperviewMargins = toastController.view.layoutMarginsGuide self.toastRenderType = toastRenderType super.init(frame: frame) configureToastType() layoutToastConstraints() } // 从屏幕底部向上动画,动画后按钮无法点击 func animateToast(){ UIView.animateKeyframes(withDuration: 4.6, delay: 0.0, options: [], animations: { UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.065, animations: { [weak self] in self?.transform = CGAffineTransform(translationX: 0, y: -40) }) UIView.addKeyframe(withRelativeStartTime: 0.93, relativeDuration: 0.065, animations: { [weak self] in self?.transform = CGAffineTransform(translationX: 0, y: 50) }) }) { (completed) in self.removeFromSuperview() } } // 配置内部文本和配色 func configureToastType(){ if self.toastRenderType == "Ok" { self.backgroundColor = UIColor(hue: 0.4222, saturation: 0.6, brightness: 0.78, alpha: 1.0) label.text = "Configuration saved!" dismissButton.setTitle("OK", for: .normal) imageView.image = UIImage(named: "checkmark.png") } else{ self.backgroundColor = UIColor(red: 0.87, green: 0.28, blue: 0.44, alpha: 1.00) label.text = "Configuration deleted." dismissButton.setTitle("Undo", for: .normal) dismissButton.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .semibold) imageView.image = UIImage(named: "close2x.png") } dismissButton.addTarget(self, action: #selector(clickedToast), for: .touchUpInside) } // 在控制器上布局,初始放在屏幕底部 func layoutOnController(){ let margins = self.toastSuperviewMargins self.top = self.topAnchor.constraint(equalTo: margins.bottomAnchor, constant: 0) self.width = self.widthAnchor.constraint(equalTo: margins.widthAnchor, multiplier: 0.92) self.height = self.heightAnchor.constraint(equalToConstant: 48) self.leading = self.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 16) self.trailing = self.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 16) NSLayoutConstraint.activate([ self.top, self.width, self.height, self.leading, self.trailing ]) } // 布局内部子视图 func layoutToastConstraints(){ self.translatesAutoresizingMaskIntoConstraints = false layer.masksToBounds = true layer.cornerRadius = 8 addSubview(label) addSubview(imageView) addSubview(dismissButton) NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 48), label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -69), label.heightAnchor.constraint(equalToConstant: 20), label.widthAnchor.constraint(equalToConstant: 226), label.topAnchor.constraint(equalTo: self.topAnchor, constant: 14), label.bottomAnchor.constraint(equalTo: self.topAnchor, constant: -14), imageView.heightAnchor.constraint(equalToConstant: 20), imageView.widthAnchor.constraint(equalToConstant: 20), imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 27), imageView.trailingAnchor.constraint(equalTo: label.leadingAnchor, constant: -10.33), imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 20.5), imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -20.17), dismissButton.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 24), dismissButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), dismissButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 16), dismissButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -16) ]) self.layoutIfNeeded() } @objc func clickedToast(){ print("you clicked the toast button") self.removeFromSuperview() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
测试控制器代码
import UIKit import CoreGraphics class ToastViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.view.backgroundColor = .white toastTest() } // 测试Toast,type决定颜色 func toastTest() -> () { let toastView = ToastView(toastRenderType: "Ok", toastController: self, frame: .zero) view.addSubview(toastView) toastView.layoutOnController() // 单独调用这个,按钮可点击 toastView.animateToast() // 执行动画后,按钮无响应 } }
解决方案
问题根源
用transform做平移时,视图的视觉位置移动了,但实际的布局边界(hitTest点击检测区域)还停留在原来的屏幕外位置,所以点击不到按钮。你之前尝试修改bottom约束没生效,是因为代码里根本没激活bottom约束,而是用top约束把视图钉在父视图margin的底部,修改bottom常量自然没用。
修复步骤
- 调整布局逻辑,用
bottom约束控制Toast的位置,初始让它完全在屏幕外 - 动画时修改
bottom约束的常量,通过约束动画实现平移,这样视图的实际布局位置会更新,hitTest区域也跟着移动 - 修复内部子视图的错误约束(比如label的bottom约束写反了)
修改后的关键代码
1. 重写layoutOnController方法
func layoutOnController(){ let margins = self.toastSuperviewMargins // 用bottom约束控制位置,初始让视图在屏幕外(高度48,所以constant设为-48) self.bottom = self.bottomAnchor.constraint(equalTo: margins.bottomAnchor, constant: -48) self.width = self.widthAnchor.constraint(equalTo: margins.widthAnchor, multiplier: 0.92) self.height = self.heightAnchor.constraint(equalToConstant: 48) self.leading = self.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 16) self.trailing = self.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: -16) NSLayoutConstraint.activate([ self.bottom, self.width, self.height, self.leading, self.trailing ]) // 立即更新布局,确保初始位置正确 self.superview?.layoutIfNeeded() }
2. 重写animateToast方法
func animateToast(){ UIView.animateKeyframes(withDuration: 4.6, delay: 0.0, options: [.allowUserInteraction], animations: { UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.065, animations: { [weak self] in // 修改bottom常量,让视图滑到屏幕内 self?.bottom.constant = 16 self?.superview?.layoutIfNeeded() }) UIView.addKeyframe(withRelativeStartTime: 0.93, relativeDuration: 0.065, animations: { [weak self] in // 滑回屏幕外 self?.bottom.constant = -48 self?.superview?.layoutIfNeeded() }) }) { (completed) in self.removeFromSuperview() } }
3. 修复内部label的错误约束
在layoutToastConstraints里,把label的bottom约束修改为:
label.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -14),
额外说明
- 添加
.allowUserInteraction动画选项,确保动画过程中按钮能响应点击 - 点击按钮时会直接移除视图,无需等待动画结束
内容的提问来源于stack exchange,提问作者kelsey-debug
相关产品推荐
相关产品推荐

