使用UIView.animate时UICollisionBehavior未触发碰撞检测的问题
问题:UICollisionBehavior代理未触发(使用UIView.animate替代UIGravityBehavior时)
已配置UIDynamicBehavior和UICollisionBehavior,但代理方法无法识别任何碰撞。因不想用UIGravityBehavior处理下落动画,改用UIView.animate实现,但碰撞代理方法完全未触发。
相关代码
class Test: UIViewController { let collision = UICollisionBehavior() let animator: UIDynamicAnimator? override func viewDidLoad() { /// view logic here animator = UIDynamicAnimator(referenceView: view) animator?.addBehavior(boundaryCollisionBehavior) boundaryCollisionBehavior.collisionDelegate = self boundaryCollisionBehavior.collisionMode = .boundaries setupViews() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) boundaryCollisionBehavior.addBoundary( withIdentifier: "shelf" as NSCopying, from: CGPoint(x: 0, y: 200), to: CGPoint(x: self.view.frame.width, y: 200) ) } func setupViews() { let viewsForCollision: [UIView] = [view1, view2, view3] UIView.animate(withDuration: animationTime, delay: 0, options: [.curveLinear], animations: { viewsForCollision.forEach { $0?.transform = .identity } }) viewsForCollision.compactMap { $0 }.forEach(boundaryCollisionBehavior.addItem) } } extension Test: UICollisionBehaviorDelegate { func collisionBehavior( _ behavior: UICollisionBehavior, endedContactFor item: UIDynamicItem, withBoundaryIdentifier identifier: NSCopying? ) { guard let boundary = identifier as? String else { return } if boundary == "shelf" { // Do something } } }
问题原因
UIDynamicAnimator的碰撞检测仅追踪由动力学行为驱动的视图运动,而UIView.animate是基于Core Animation的属性动画,其视图位置变化不会被UIDynamicAnimator监控。即使将视图添加到UICollisionBehavior中,动画过程中视图的位置更新无法被动力学系统感知,因此不会触发碰撞代理方法。
解决方案
必须让UIDynamicAnimator驱动视图运动,才能触发碰撞检测。以下是两种可行方案:
方案1:改用UIDynamics行为实现下落动画
如果需要线性下落效果,可使用UIDynamicItemBehavior设置恒定速度,替代UIView.animate:
class Test: UIViewController { let collision = UICollisionBehavior() var animator: UIDynamicAnimator? // 修改为可变属性,避免初始化后被释放 override func viewDidLoad() { super.viewDidLoad() animator = UIDynamicAnimator(referenceView: view) // 修正变量名不一致问题(原代码中boundaryCollisionBehavior应为collision) animator?.addBehavior(collision) collision.collisionDelegate = self collision.collisionMode = .boundaries setupViews() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) collision.addBoundary( withIdentifier: "shelf" as NSCopying, from: CGPoint(x: 0, y: 200), to: CGPoint(x: self.view.frame.width, y: 200) ) } func setupViews() { let viewsForCollision = [view1, view2, view3].compactMap { $0 } // 先设置视图初始位置(比如上方) viewsForCollision.forEach { $0.transform = CGAffineTransform(translationX: 0, y: -300) collision.addItem($0) } // 用UIDynamicItemBehavior实现线性下落 let itemBehavior = UIDynamicItemBehavior(items: viewsForCollision) itemBehavior.addLinearVelocity(CGVector(dx: 0, dy: 500), for: viewsForCollision.first!) // 设置初始垂直速度 itemBehavior.linearDamping = 0 // 无阻尼,保持匀速运动 animator?.addBehavior(itemBehavior) } } extension Test: UICollisionBehaviorDelegate { func collisionBehavior( _ behavior: UICollisionBehavior, endedContactFor item: UIDynamicItem, withBoundaryIdentifier identifier: NSCopying? ) { guard let boundary = identifier as? String, boundary == "shelf" else { return } // 碰撞后的逻辑 print("碰撞到shelf边界") } }
方案2:手动同步UIView.animate的位置到动力学系统(不推荐)
如果必须使用UIView.animate,可在动画闭包中手动更新动力学系统中item的位置,但无法实时触发碰撞检测,仅能在动画结束后检查:
UIView.animate(withDuration: animationTime, delay: 0, options: [.curveLinear], animations: { viewsForCollision.forEach { view in view?.transform = .identity // 手动更新动力学系统中item的位置 if let item = view as? UIDynamicItem { self.collision.updateItem(usingCurrentState: item) } } }) { _ in // 动画结束后检查是否碰撞 viewsForCollision.forEach { view in guard let item = view as? UIDynamicItem else { return } let isColliding = self.collision.items(in: self.view.bounds).contains(item) // 处理碰撞逻辑 } }
关键注意点
- 确保
UIDynamicAnimator实例被正确初始化并持有(原代码中animator为不可变可选类型,初始化后可能被释放,需改为可变属性) - 修正变量名不一致问题:原代码中声明的是
collision,但使用时写的是boundaryCollisionBehavior,需统一 - 碰撞模式设置为
.boundaries后,需确保边界已正确添加(viewDidAppear中添加边界是正确时机,此时view的frame已确定)
内容的提问来源于stack exchange,提问作者swiftcode123
相关产品推荐
相关产品推荐

