iOS:如何在UIView动画块中正确交换两个UIView的位置?
问题描述
需求是点击按钮时让两个UIView(A为fromAccountCardView、B为toAccountCardView)交换位置:A向下移动到B的初始位置,B向上移动到A的初始位置。
直接在UIView.animate块中交换两个视图及其内部子视图的.center属性后,动画效果完全不符合预期:A会先向上移出屏幕,再从底部出现到达目标位置;B则先向下移出屏幕,再从顶部出现到达目标位置。
原实现代码:
fileprivate func performSwitchAnimation() { // NOTE: ownAccountTransferView is a reference to my view, think of it like self.view let fromAccountCardCenter = self.ownAccountTransferView.fromAccountCardView.center let fromAccountTypeLabelCenter = self.ownAccountTransferView.fromAccountTypeLabel.center let fromAccountTypeViewCenter = self.ownAccountTransferView.fromAccountTypeView.center let fromAccountBalanceLabelCenter = self.ownAccountTransferView.fromAccountBalanceLabel.center let fromAccountNameLabelCenter = self.ownAccountTransferView.fromAccountNameLabel.center let fromAccountChevronCenter = self.ownAccountTransferView.fromAccountChevronView.center let toAccountCardCenter = self.ownAccountTransferView.toAccountCardView.center let toAccountTypeLabelCenter = self.ownAccountTransferView.toAccountTypeLabel.center let toAccountTypeViewCenter = self.ownAccountTransferView.toAccountTypeView.center let toAccountBalanceLabelCenter = self.ownAccountTransferView.toAccountBalanceLabel.center let toAccountNameLabelCenter = self.ownAccountTransferView.toAccountNameLabel.center let toAccountChevronCenter = self.ownAccountTransferView.toAccountChevronView.center UIView.animate(withDuration: 1, delay: 0, options: []) { self.ownAccountTransferView.switchAccountsButton.isUserInteractionEnabled = false self.ownAccountTransferView.fromAccountCardView.center = toAccountCardCenter self.ownAccountTransferView.fromAccountTypeLabel.center = toAccountTypeLabelCenter self.ownAccountTransferView.fromAccountTypeView.center = toAccountTypeViewCenter self.ownAccountTransferView.fromAccountBalanceLabel.center = toAccountBalanceLabelCenter self.ownAccountTransferView.fromAccountNameLabel.center = toAccountNameLabelCenter self.ownAccountTransferView.fromAccountChevronView.center = toAccountChevronCenter self.ownAccountTransferView.toAccountCardView.center = fromAccountCardCenter self.ownAccountTransferView.toAccountTypeLabel.center = fromAccountTypeLabelCenter self.ownAccountTransferView.toAccountTypeView.center = fromAccountTypeViewCenter self.ownAccountTransferView.toAccountBalanceLabel.center = fromAccountBalanceLabelCenter self.ownAccountTransferView.toAccountNameLabel.center = fromAccountNameLabelCenter self.ownAccountTransferView.toAccountChevronView.center = fromAccountChevronCenter } completion: { isDone in if isDone { self.ownAccountTransferView.switchAccountsButton.isUserInteractionEnabled = true } } }
问题根源
直接修改.center属性的做法,在Auto Layout布局环境下会触发约束冲突——视图的实际位置由约束控制,手动修改center后,Auto Layout会在动画过程中试图修正位置,导致动画路径出现异常的“绕路”情况。同时逐个修改子视图的center不仅冗余,还容易造成动画不同步。
可行解决方案
方案1:使用Transform实现(已验证有效)
通过CGAffineTransform的平移动画,绕过Auto Layout的约束干扰,直接控制视图的视觉位置,动画完成后再重置transform并交换数据:
fileprivate func performSwitchAnimation() { let ownView = self.ownAccountTransferView ownView.switchAccountsButton.isUserInteractionEnabled = false // 计算两个视图的垂直偏移量:from需要向下移动的距离 let offsetY = ownView.toAccountCardView.frame.minY - ownView.fromAccountCardView.frame.minY UIView.animate(withDuration: 1, delay: 0, options: .curveEaseInOut) { // from向下平移,to向上平移 ownView.fromAccountCardView.transform = CGAffineTransform(translationX: 0, y: offsetY) ownView.toAccountCardView.transform = CGAffineTransform(translationX: 0, y: -offsetY) } completion: { [weak self] isDone in guard let self = self else { return } let ownView = self.ownAccountTransferView // 重置transform,同时交换两个视图的业务数据 ownView.fromAccountCardView.transform = .identity ownView.toAccountCardView.transform = .identity // 此处替换为实际的数据交换逻辑 let tempAccountData = ownView.fromAccountData ownView.fromAccountData = ownView.toAccountData ownView.toAccountData = tempAccountData ownView.switchAccountsButton.isUserInteractionEnabled = true } }
注意:transform会作用于整个视图及其所有子视图,无需单独给子视图加动画;恢复位置时必须用
.identity重置,不能复用原偏移值,否则会出现异常。
方案2:Auto Layout约束动画(适合依赖约束的场景)
如果必须基于Auto Layout实现,要通过修改约束的constant值而非直接改center:
- 给两个视图的垂直位置约束(如
centerY或top约束)创建IBOutlet - 交换约束的constant值后,在动画块中调用
layoutIfNeeded()触发动画
示例代码:
// 假设已创建两个约束的IBOutlet @IBOutlet weak var fromCardCenterYConstraint: NSLayoutConstraint! @IBOutlet weak var toCardCenterYConstraint: NSLayoutConstraint! fileprivate func performSwitchAnimation() { let ownView = self.ownAccountTransferView ownView.switchAccountsButton.isUserInteractionEnabled = false // 交换两个约束的constant值 let tempConstant = fromCardCenterYConstraint.constant fromCardCenterYConstraint.constant = toCardCenterYConstraint.constant toCardCenterYConstraint.constant = tempConstant UIView.animate(withDuration: 1, delay: 0, options: .curveEaseInOut) { ownView.layoutIfNeeded() } completion: { isDone in ownView.switchAccountsButton.isUserInteractionEnabled = true // 交换业务数据 } }
注意:要确保没有其他垂直约束干扰视图位置,否则仍可能出现动画异常。
总结
直接修改center属性在Auto Layout环境下极易触发约束冲突,导致动画路径异常。使用CGAffineTransform平移是更简洁可靠的方案;如果依赖Auto Layout,必须通过修改约束并调用layoutIfNeeded()来实现动画。
内容的提问来源于stack exchange,提问作者Pablo

