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

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常量自然没用。

修复步骤

  1. 调整布局逻辑,用bottom约束控制Toast的位置,初始让它完全在屏幕外
  2. 动画时修改bottom约束的常量,通过约束动画实现平移,这样视图的实际布局位置会更新,hitTest区域也跟着移动
  3. 修复内部子视图的错误约束(比如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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 00:03:18