Что такое Core Animation и как создавать базовые анимации в UIKit?
Core Animation работает через CALayer (model/presentation layer), render server и CATransaction. UIView.animate — удобная обёртка; CABasicAnimation даёт контроль над timing и fill mode; CADisplayLink синхронизирует покадровую логику с дисплеем.
Что такое Core Animation
Core Animation (CA) — графический движок Apple, работающий в отдельном процессе rendererd (iOS) или WindowServer (macOS). Он оперирует не UIView, а CALayer — backing layer каждого вью. Слои хранят два дерева состояния: model layer (то, что вы присваиваете в коде) и presentation layer (интерполированное значение в данный кадр). Это различие — источник большинства багов с hit-testing во время анимации.
UIKit предоставляет три уровня абстракции над CA:
- UIView.animate(withDuration:…) — самый высокий, анимирует animatable-свойства вью через implicit transaction;
- CABasicAnimation / CAKeyframeAnimation — явные анимации на уровне слоя, дают полный контроль над timing, fill mode и делегатом;
- CADisplayLink — покадровый callback, синхронизированный с частотой дисплея (ProMotion: до 120 Hz).
Пример: UIView.animate и CABasicAnimation
import UIKit
final class AnimationDemoViewController: UIViewController {
private let box = UIView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
box.frame = CGRect(x: 40, y: 200, width: 80, height: 80)
box.backgroundColor = .systemBlue
box.layer.cornerRadius = 12
view.addSubview(box)
let tapGR = UITapGestureRecognizer(target: self, action: #selector(runAnimations))
view.addGestureRecognizer(tapGR)
}
// MARK: – UIView convenience API (implicit CATransaction)
@objc private func runAnimations() {
// 1. Простая анимация позиции и прозрачности
UIView.animate(
withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5,
options: [.curveEaseInOut]
) {
self.box.center.x += 120
self.box.alpha = 0.4
} completion: { _ in
// completion вызывается на главном потоке
self.runCABasicAnimation()
}
}
// MARK: – Явная CABasicAnimation
private func runCABasicAnimation() {
let anim = CABasicAnimation(keyPath: "transform.rotation.z")
anim.fromValue = 0
anim.toValue = Double.pi * 2 // полный оборот
anim.duration = 0.8
anim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
// fillMode + isRemovedOnCompletion — частая ловушка
anim.fillMode = .forwards
anim.isRemovedOnCompletion = false // ⚠️ слой живёт вечно, если забыть убрать
anim.delegate = self // AnimationDelegate
box.layer.add(anim, forKey: "rotation")
}
// MARK: – CAKeyframeAnimation для нелинейного пути
private func runKeyframe() {
let pathAnim = CAKeyframeAnimation(keyPath: "position")
pathAnim.path = makeBouncePath()
pathAnim.duration = 1.2
pathAnim.calculationMode = .cubicPaced
box.layer.add(pathAnim, forKey: "pathMove")
}
private func makeBouncePath() -> CGPath {
let path = UIBezierPath()
path.move(to: box.center)
path.addQuadCurve(
to: CGPoint(x: box.center.x, y: 500),
controlPoint: CGPoint(x: box.center.x + 80, y: 350)
)
return path.cgPath
}
}
// MARK: – CAAnimationDelegate
extension AnimationDemoViewController: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
guard flag else { return } // отменена — не трогаем UI
// Синхронизируем model layer с presentation layer
box.layer.removeAnimation(forKey: "rotation")
// Сбрасываем реальное значение, иначе при следующем изменении будет прыжок
box.layer.transform = CATransform3DIdentity
}
}
CADisplayLink для покадровой анимации
final class ParticleViewController: UIViewController {
private var displayLink: CADisplayLink?
private var phase: CGFloat = 0
private let waveLayer = CAShapeLayer()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
view.layer.addSublayer(waveLayer)
waveLayer.strokeColor = UIColor.systemIndigo.cgColor
waveLayer.fillColor = UIColor.clear.cgColor
waveLayer.lineWidth = 2
displayLink = CADisplayLink(target: self, selector: #selector(tick))
// preferredFrameRateRange — ProMotion API (iOS 15+)
displayLink?.preferredFrameRateRange = CAFrameRateRange(
minimum: 60, maximum: 120, preferred: 120
)
displayLink?.add(to: .main, forMode: .common)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
displayLink?.invalidate() // ⚠️ без invalidate — retain cycle и утечка
displayLink = nil
}
@objc private func tick(link: CADisplayLink) {
// link.targetTimestamp — время следующего кадра (точнее, чем Date())
phase += CGFloat(link.targetTimestamp - link.timestamp) * 3
waveLayer.path = buildWavePath(phase: phase)
}
private func buildWavePath(phase: CGFloat) -> CGPath {
let path = UIBezierPath()
let w = view.bounds.width
path.move(to: CGPoint(x: 0, y: view.bounds.midY))
stride(from: CGFloat(0), through: w, by: 2).forEach { x in
let y = view.bounds.midY + sin(x / 30 + phase) * 40
path.addLine(to: CGPoint(x: x, y: y))
}
return path.cgPath
}
}
Как работает render pipeline
При изменении свойства слоя CA создаёт неявный CATransaction. В конце run loop он фиксирует дерево, отправляет скомпилированные команды в render server по XPC. Render server работает в отдельном процессе — именно поэтому анимации не прерываются при занятом главном потоке короткое время. Но длинный main-thread jank (~16 мс) всё равно вызывает dropped frames, потому что CA не получает следующий commit.
Подводные камни
- fillMode + isRemovedOnCompletion = false без cleanup. Presentation layer замирает в конечной точке, model layer — в исходной. При следующем изменении происходит видимый прыжок. Всегда явно задавайте финальное значение model layer в
animationDidStopи затем вызывайтеremoveAnimation(forKey:). - Hit-testing бьёт по model layer. Во время анимации
UIView.animateнажатие в визуальную позицию кнопки ничего не даёт — responder chain ищет view поframeиз model layer (исходная позиция). Используйтеlayer.presentation()?.hitTest(point)или перемещайте view до начала анимации. - CADisplayLink и retain cycle.
CADisplayLinkудерживает target сильной ссылкой. Если target — UIViewController, и вы забылиinvalidate()вviewDidDisappear, контроллер никогда не освободится. Альтернатива — weak proxy через NSProxy. - Анимация после
layoutSubviews. Если изменитьframeвью внутри блока анимации, а затем вызватьsetNeedsLayout— AutoLayout пересчитает frame синхронно и отменит анимацию. Анимируйте constraint constant, а не frame напрямую. - preferredFramesPerSecond устарел. С iOS 15 используйте
CAFrameRateRange. Старое API игнорируется на ProMotion-устройствах с адаптивной частотой. Подводные камни
- Проверяйте корректность данных после каждой операции.
Common mistakes
- Сводить «Core Animation и как создавать базовые анимации в UIKit» к синтаксису и не объяснять responder chain.
- Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии uikit-18.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «Core Animation и как создавать базовые анимации в UIKit» и подтверждает ее корректным примером.
- Умеет связать ответ с reuse pool, тестированием и отладкой на устройстве.
- Называет ограничения подхода uikit-18, включая производительность, память и сопровождение.