iOS Swift中检测动画UIView与其他视图相交/中心重合的方案
解决Label动画中的相交与中心重合检测问题
你之前用CADisplayLink没达到预期,大概率是直接取了UILabel的frame——Core Animation动画运行时,视图的frame不会实时更新(动画在presentation layer上执行,而视图本身的layer保留初始/最终状态)。下面给两种可行的实现方案:
方案一:用Presentation Layer实时检测位置
通过获取Label1的presentationLayer(当前屏幕上显示的实时图层状态)来拿到动画中的真实位置,配合CADisplayLink每帧检测:
import UIKit class ViewController: UIViewController { var displayLink: CADisplayLink? let label1 = UILabel() let labels: [UILabel] = [UILabel(), UILabel(), UILabel(), UILabel()] // Label2-5 // 记录已触发的事件,避免重复回调 private var triggeredIntersects = Set<Int>() private var triggeredCenters = Set<Int>() override func viewDidLoad() { super.viewDidLoad() setupLabels() setupDisplayLink() startAnimation() } private func setupLabels() { // 示例布局:Label1初始在左侧,Label2-5依次排列在右侧 label1.frame = CGRect(x: 20, y: 100, width: 80, height: 40) label1.backgroundColor = .red label1.text = "Label1" view.addSubview(label1) for (index, label) in labels.enumerated() { label.frame = CGRect(x: 200 + index*100, y: 100, width: 80, height: 40) label.backgroundColor = .blue label.text = "Label\(index+2)" view.addSubview(label) } } private func setupDisplayLink() { displayLink = CADisplayLink(target: self, selector: #selector(checkFrameStatus)) displayLink?.add(to: .main, forMode: .common) } @objc private func checkFrameStatus() { guard let label1PresentFrame = label1.layer.presentation()?.frame else { return } let label1Center = CGPoint(x: label1PresentFrame.midX, y: label1PresentFrame.midY) for (labelIndex, targetLabel) in labels.enumerated() { let targetFrame = targetLabel.frame let targetCenter = CGPoint(x: targetFrame.midX, y: targetFrame.midY) let actualLabelIndex = labelIndex + 2 // 检测相交 if label1PresentFrame.intersects(targetFrame) && !triggeredIntersects.contains(actualLabelIndex) { print("Label1 与 Label\(actualLabelIndex) 相交") triggeredIntersects.insert(actualLabelIndex) } // 检测中心重合(用1像素阈值避免错过完全重合的帧) let centerDistance = hypot(label1Center.x - targetCenter.x, label1Center.y - targetCenter.y) if centerDistance < 1.0 && !triggeredCenters.contains(actualLabelIndex) { print("Label1 与 Label\(actualLabelIndex) 中心重合") triggeredCenters.insert(actualLabelIndex) } } } private func startAnimation() { UIView.animate(withDuration: 3.0) { self.label1.frame.origin.x = self.view.bounds.width - self.label1.bounds.width - 20 } completion: { _ in self.displayLink?.invalidate() self.displayLink = nil } } }
方案二:用UIViewPropertyAnimator监听进度(更高效)
如果是线性动画,可以提前计算触发相交/中心重合的动画进度,通过UIViewPropertyAnimator的进度监听直接触发事件,不需要每帧做碰撞检测:
import UIKit class ViewController: UIViewController { var animator: UIViewPropertyAnimator? let label1 = UILabel() let labels: [UILabel] = [UILabel(), UILabel(), UILabel(), UILabel()] private var triggeredIntersects = Set<Int>() private var triggeredCenters = Set<Int>() override func viewDidLoad() { super.viewDidLoad() setupLabels() setupAnimator() } private func setupLabels() { // 同方案一的布局代码 label1.frame = CGRect(x: 20, y: 100, width: 80, height: 40) label1.backgroundColor = .red label1.text = "Label1" view.addSubview(label1) for (index, label) in labels.enumerated() { label.frame = CGRect(x: 200 + index*100, y: 100, width: 80, height: 40) label.backgroundColor = .blue label.text = "Label\(index+2)" view.addSubview(label) } } private func setupAnimator() { let startX = label1.frame.origin.x let endX = view.bounds.width - label1.bounds.width - 20 let totalMoveDistance = endX - startX // 提前计算每个Label对应的触发进度 var triggerPoints: [(labelIndex: Int, intersectProgress: CGFloat, centerProgress: CGFloat)] = [] for (index, targetLabel) in labels.enumerated() { let actualLabelIndex = index + 2 // 相交触发点:Label1右边缘碰到目标Label左边缘的x位置 let intersectTriggerX = targetLabel.frame.origin.x - label1.bounds.width let intersectProgress = (intersectTriggerX - startX) / totalMoveDistance // 中心重合触发点:Label1中心与目标Label中心重合的x位置 let centerTriggerX = targetLabel.frame.midX - label1.bounds.width/2 let centerProgress = (centerTriggerX - startX) / totalMoveDistance triggerPoints.append((actualLabelIndex, intersectProgress, centerProgress)) } // 创建动画器 animator = UIViewPropertyAnimator(duration: 3.0, curve: .linear) { self.label1.frame.origin.x = endX } // 添加进度监听(每60帧检测一次,和屏幕刷新同步) animator?.addProgressObserver(forInterval: 1/60, queue: .main) { [weak self] progress in guard let self = self else { return } for point in triggerPoints { // 触发相交事件 if progress >= point.intersectProgress && !self.triggeredIntersects.contains(point.labelIndex) { print("Label1 与 Label\(point.labelIndex) 相交") self.triggeredIntersects.insert(point.labelIndex) } // 触发中心重合事件 if progress >= point.centerProgress && !self.triggeredCenters.contains(point.labelIndex) { print("Label1 与 Label\(point.labelIndex) 中心重合") self.triggeredCenters.insert(point.labelIndex) } } } animator?.startAnimation() } }
方案对比
- 方案一:适配所有动画类型(包括弹簧、非线性曲线动画),但需要每帧做碰撞检测,且要处理重复触发问题。
- 方案二:性能更优,触发时机精准,不会重复回调,但仅适合可提前计算位置的动画(如果是非线性动画,需要用
CAMediaTimingFunction转换进度和实际位移的关系)。
内容的提问来源于stack exchange,提问作者user4496950
相关产品推荐
相关产品推荐

