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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 14:00:47