Реализация Spring-анимаций (физических) в iOS-приложении
Spring-анимации делают интерфейс живым — элементы не просто перемещаются, они «пружинят», слегка перелетая целевую точку и возвращаясь обратно. iOS использует их везде: иконки при долгом нажатии, карточки в App Store, клавиши клавиатуры. Разница между «сделано как в системе» и «почти как в системе» — в правильных параметрах физической модели.
UIKit: UISpringTimingParameters и UIViewPropertyAnimator
До iOS 10 spring-анимации делали через UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:). Параметры dampingRatio (0–1) и initialVelocity работают, но это упрощённая модель — не настоящая физика пружины.
Начиная с iOS 10 — UISpringTimingParameters с mass, stiffness и damping:
let timingParams = UISpringTimingParameters(
mass: 1.0,
stiffness: 170,
damping: 26,
initialVelocity: CGVector(dx: 0, dy: 0)
)
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParams)
animator.addAnimations {
self.cardView.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
}
animator.startAnimation()
duration: 0 — при использовании spring timing duration игнорируется, анимация длится столько, сколько нужно пружине для затухания. Это правильно — не задавайте duration для spring.
Параметры для типичных случаев: stiffness: 300, damping: 30 — быстрая упругая анимация (feedback при нажатии). stiffness: 120, damping: 14 — медленная мягкая пружина (появление bottom sheet). stiffness: 400, damping: 40 — жёсткая без перелёта (переключатель).
UIViewPropertyAnimator поддерживает isInterruptible = true — анимацию можно остановить и перенаправить в процессе. Это важно для gesture-driven интерфейсов: если пользователь начал тянуть карточку вниз и передумал — анимация плавно реверсируется с текущей скорости.
SwiftUI: spring() и .interpolatingSpring()
SwiftUI предлагает несколько вариантов:
// Простой spring с dampingFraction
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
isExpanded.toggle()
}
// Физическая модель через interpolatingSpring
withAnimation(.interpolatingSpring(stiffness: 170, damping: 26)) {
offset = targetOffset
}
// iOS 17+: новый Spring тип
withAnimation(.spring(.bouncy(duration: 0.4, extraBounce: 0.1))) {
scale = 1.0
}
.spring(response:dampingFraction:) — проще в использовании: response — это приблизительная длительность (но не жёсткая), dampingFraction 1.0 — критическое затухание без перелёта, меньше 1.0 — перелёт. Для большинства UI-анимаций: response: 0.3–0.5, dampingFraction: 0.7–0.85.
iOS 17 принёс именованные spring presets: .bouncy, .smooth, .snappy — полезны для быстрого прототипирования, но для финального продукта лучше задавать параметры явно.
Velocity matching при прерывании: когда анимация прерывается жестом, новая spring-анимация должна начинаться с текущей скорости. В SwiftUI это через @GestureState и withAnimation с правильным initialVelocity. В UIKit — через UIViewPropertyAnimator.fractionComplete и continueAnimation(withTimingParameters:durationFactor:).
Gesture-driven spring: UIPanGestureRecognizer + spring
Самый живой кейс — карточка, которую можно тянуть, и она spring-ится обратно или долетает до следующей позиции:
@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: view)
switch gesture.state {
case .changed:
cardView.transform = CGAffineTransform(translationX: 0, y: translation.y)
case .ended:
let velocity = gesture.velocity(in: view)
let velocityVector = CGVector(dx: 0, dy: velocity.y / 1000) // нормализация
let timingParams = UISpringTimingParameters(
mass: 1, stiffness: 200, damping: 28,
initialVelocity: velocityVector
)
let animator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParams)
animator.addAnimations {
self.cardView.transform = .identity
}
animator.startAnimation()
default: break
}
}
initialVelocity берём из gesture velocity, нормализуем делением на ~1000 (порядок величин UISpringTimingParameters отличается от points/second жеста).
Сроки
Добавление spring-анимаций к существующим UI-компонентам (2–4 элемента): 1–2 дня с тестированием на устройствах. Gesture-driven интерактивный экран с spring physics — 2–3 дня. Стоимость рассчитывается индивидуально.







