Пол Попил дал превосходный ответ на этот вопрос выше, и я ему за это в долгу. Есть одна небольшая проблема, которую я обнаружил с его кодом, и это то, что он не работает хорошо, если эта процедура вызывается несколько раз - анимации слоев иногда теряются или деактивируются.
Зачем называть это более одного раза? Я реализую это через UICollectionView, и, поскольку ячейки убраны или перемещены, мне нужно восстановить покачивание. С оригинальным кодом Пола мои ячейки часто перестали бы покачиваться, если бы они прокручивались вне экрана, несмотря на мои попытки восстановить покачивание в пределах очереди и обратный вызов willDisplay. Однако, давая две анимации с именем keys, он всегда работает надежно, даже если дважды вызывается в ячейке.
Это почти весь код Пола с небольшим исправлением, описанным выше, плюс я создал его как расширение UIView и добавил Swift 4-совместимый stopWiggle.
private func degreesToRadians(_ x: CGFloat) -> CGFloat {
return .pi * x / 180.0
}
extension UIView {
func startWiggle() {
let duration: Double = 0.25
let displacement: CGFloat = 1.0
let degreesRotation: CGFloat = 2.0
let negativeDisplacement = -1.0 * displacement
let position = CAKeyframeAnimation.init(keyPath: "position")
position.beginTime = 0.8
position.duration = duration
position.values = [
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
NSValue(cgPoint: CGPoint(x: 0, y: 0)),
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)),
NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
]
position.calculationMode = "linear"
position.isRemovedOnCompletion = false
position.repeatCount = Float.greatestFiniteMagnitude
position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
position.isAdditive = true
let transform = CAKeyframeAnimation.init(keyPath: "transform")
transform.beginTime = 2.6
transform.duration = duration
transform.valueFunction = CAValueFunction(name: kCAValueFunctionRotateZ)
transform.values = [
degreesToRadians(-1.0 * degreesRotation),
degreesToRadians(degreesRotation),
degreesToRadians(-1.0 * degreesRotation)
]
transform.calculationMode = "linear"
transform.isRemovedOnCompletion = false
transform.repeatCount = Float.greatestFiniteMagnitude
transform.isAdditive = true
transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
self.layer.add(position, forKey: "bounce")
self.layer.add(transform, forKey: "wiggle")
}
func stopWiggle() {
self.layer.removeAllAnimations()
self.transform = .identity
}
}
В случае, если это сэкономит кому-либо еще время при реализации этого в UICollectionView, вам понадобится несколько других мест, чтобы убедиться, что покачивание остается во время перемещений и прокрутки. Во-первых, процедура, которая начинает шевелить все ячейки, которые вызываются с самого начала:
func wiggleAllVisibleCells() {
if let visible = collectionView?.indexPathsForVisibleItems {
for ip in visible {
if let cell = collectionView!.cellForItem(at: ip) {
cell.startWiggle()
}
}
}
}
И, когда отображаются новые ячейки (из движения или прокрутки), я восстанавливаю движение:
override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// Make sure cells are all still wiggling
if isReordering {
cell.startWiggle()
}
}