Создание анимаций переходов между экранами мобильного приложения
Переход между экранами — момент, когда пользователь либо понимает, куда он перемещается, либо теряется. Плохой переход не просто некрасив: он создаёт когнитивную нагрузку. Стандартный pushViewController на iOS или startActivity на Android работают, но не создают ощущения связности интерфейса.
Платформенные механизмы навигационных переходов
UIKit и кастомные transitions на iOS
UIKit предоставляет два уровня кастомизации. Первый — UINavigationControllerDelegate с методом navigationController(_:animationControllerFor:from:to:). Возвращаешь свой объект, реализующий UIViewControllerAnimatedTransitioning, и получаешь полный контроль над анимацией.
class SlideUpTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.38
}
func animateTransition(using ctx: UIViewControllerContextTransitioning) {
guard let toVC = ctx.viewController(forKey: .to),
let fromVC = ctx.viewController(forKey: .from) else { return }
let container = ctx.containerView
let finalFrame = ctx.finalFrame(for: toVC)
toVC.view.frame = finalFrame.offsetBy(dx: 0, dy: finalFrame.height)
container.addSubview(toVC.view)
UIView.animate(
withDuration: transitionDuration(using: ctx),
delay: 0,
usingSpringWithDamping: 0.88,
initialSpringVelocity: 0.3,
options: [.curveEaseOut]
) {
toVC.view.frame = finalFrame
fromVC.view.alpha = 0.85
fromVC.view.transform = CGAffineTransform(scaleX: 0.96, y: 0.96)
} completion: { _ in
fromVC.view.transform = .identity
fromVC.view.alpha = 1
ctx.completeTransition(!ctx.transitionWasCancelled)
}
}
}
Spring damping 0.88 с velocity 0.3 — это примерно то, что Apple использует в родных переходах. Меньше — будет болтаться, больше — потеряется эффект упругости. Главная ошибка: забыть completeTransition(false) при отмене жестом назад. Без этого контроллер зависает в промежуточном состоянии.
Второй уровень — UIViewControllerInteractiveTransitioning для жестовой навигации. Связываешь UIPercentDrivenInteractiveTransition с UIPanGestureRecognizer, обновляешь update(_:) при каждом изменении позиции пальца.
SwiftUI: matchedGeometryEffect и NavigationTransition
SwiftUI даёт matchedGeometryEffect(id:in:) — декларативный Shared Element Transition. Достаточно пометить одинаковым id вью на обоих экранах в одном Namespace:
@Namespace var heroNamespace
// Список
Image(product.imageName)
.matchedGeometryEffect(id: product.id, in: heroNamespace)
// Детальный экран
Image(product.imageName)
.matchedGeometryEffect(id: product.id, in: heroNamespace)
iOS 18 добавил NavigationTransition протокол и несколько готовых эффектов через .navigationTransition(.zoom(...)). Это нативный zoom-transition как в Photos.app.
Jetpack Compose: AnimatedContent и SharedTransitionLayout
На Android с Compose переходы строятся через AnimatedContent внутри NavHost:
NavHost(
navController = navController,
startDestination = "list",
enterTransition = {
slideIntoContainer(
AnimatedContentTransitionScope.SlideDirection.Start,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
)
)
},
exitTransition = {
slideOutOfContainer(
AnimatedContentTransitionScope.SlideDirection.Start,
animationSpec = tween(300)
)
}
) { ... }
SharedTransitionLayout + sharedElement() модификатор — аналог matchedGeometryEffect для Compose, появился в Compose 1.7. До этого Shared Element в Compose был болью: библиотека Accompanist Transitions работала, но с артефактами при быстрых переходах.
Типичные грабли
Freeze на первом кадре. Бывает когда destination view контроллер ещё не завершил layout в момент начала анимации. Фикс: вызвать toVC.view.layoutIfNeeded() до старта анимации.
Jumpy status bar при переходе между экранами с разным preferredStatusBarStyle. UIKit пересчитывает стиль с задержкой. Решение: установить modalPresentationCapturesStatusBarAppearance = true на presenting контроллере.
Чёрный прямоугольник под прозрачным NavigationBar при custom transition — происходит, когда containerView.backgroundColor не установлен явно. Контейнер наследует .systemBackground, но при анимации opacity могут быть артефакты.
Платформенные гайдлайны: что нельзя нарушать
Apple Human Interface Guidelines требуют, чтобы переходы были не длиннее 400ms для навигационных действий. Android Material Design 3 — 300ms для container transforms. Превышение воспринимается как «медленный телефон», даже если приложение технически быстрое.
Modal presentations на iOS (.sheet) системно анимируются снизу вверх. Переопределять это направление без веской причины — плохая практика: пользователь привык к этому паттерну.
Процесс работы
Начинаем с аудита текущих переходов и составления карты экранов с указанием типа перехода для каждой пары. Далее — прототипирование в Xcode/Android Studio с подбором параметров spring-анимаций. Реализация кастомных UIViewControllerAnimatedTransitioning / NavigationTransition / Compose transitions. Параллельно — интерактивные переходы на жестах там, где это уместно. Финальное тестирование на реальных устройствах (включая iPhone SE 2nd gen с меньшим CPU) через Xcode Core Animation Instrument.
Ориентиры по срокам
Базовый набор переходов для 3–5 типов экранов — 2–3 рабочих дня. Кастомный Hero-transition с интерактивностью на жесте — от 3 до 5 дней.







