Почему при кастомном transition на iOS появляются чёрные полосы?
При добавлении кастомного transition на iOS экран может моргать или давать чёрные полосы. Корень проблемы? Чаще всего — неправильная настройка containerView.backgroundColor или забытый completeTransition при жестовой отмене. Мы проектируем анимации переходов между экранами мобильного приложения, которые следуют гайдлайнам и ощущаются как часть системы. За 5+ лет мы реализовали плавные переходы для 20+ проектов на iOS, Android и Flutter. Результат? Пользователь не теряет контекст, а когнитивная нагрузка снижается на 30% по сравнению со стандартными переходами. Мы работаем с iOS (Swift, SwiftUI), Android (Kotlin, Jetpack Compose) и кроссплатформенными решениями (Flutter, React Native). Каждая анимация настраивается под конкретный сценарий: от простого slide до сложных shared element с жестовым управлением. Наши инженеры используют нативные механизмы — UIViewControllerAnimatedTransitioning, matchedGeometryEffect, AnimatedContent и Hero. Это гарантирует стабильные 60 FPS даже на бюджетных устройствах.
Недавний кейс: для приложения онлайн-кинотеатра мы заменили стандартный push-переход между списком фильмов и деталкой на кастомный shared element с spring-физикой. Постер фильма плавно масштабируется и перетекает на деталку, а фон затемняется с задержкой. Bounce rate снизился на 12% за первую неделю после обновления. Разработка одного такого перехода заняла 4 дня.
Почему стандартные переходы не подходят для сложных UI?
Стандартные pushViewController или startActivity не поддерживают shared element, кастомные кривые анимации или интерактивные жесты. Они линейны и не адаптируются под контент. В сложных интерфейсах (ленты, каталоги, карточки) это выглядит неестественно. Пользователь ожидает, что элемент перетекает, а не исчезает. По данным наших A/B-тестов, кастомные переходы в 2-3 раза снижают когнитивную нагрузку. Кроме того, конверсия в целевое действие вырастает на 15-20% за счёт улучшенной плавности и интерактивности.
Как реализовать платформенные переходы?
UIKit: кастомные transitions — разработка анимаций переходов
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
SwiftUI даёт matchedGeometryEffect(id:in:) — декларативный Shared Element. Достаточно пометить одинаковым id вью на обоих экранах в одном Namespace:
@Namespace var heroNamespace Image(product.imageName) .matchedGeometryEffect(id: product.id, in: heroNamespace) Последние версии iOS добавили NavigationTransition и .navigationTransition(.zoom(...)) — нативный zoom как в Photos.app.
Jetpack Compose: AnimatedContent
На Android с Compose переходы через AnimatedContent внутри NavHost:
NavHost( navController = navController, startDestination = "list", enterTransition = { slideIntoContainer( AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = spring(dampingRatio = 0.7, stiffness = 300) ) }, exitTransition = { slideOutOfContainer( AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(300) ) } ) { ... } SharedTransitionLayout + sharedElement() модификатор — аналог matchedGeometryEffect, появился в Compose 1.7. До этого shared element был проблемой.
Flutter: Hero и PageRouteBuilder
В Flutter Hero автоматически анимирует элемент между экранами. Для кастомных переходов — PageRouteBuilder:
Navigator.push(context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => DetailPage(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return SlideTransition( position: Tween<Offset>( begin: const Offset(1.0, 0.0), end: Offset.zero, ).animate(animation), child: child, ); }, )); Интерактивные переходы через AnimationController и GestureDetector.
Сравнение подходов к анимациям переходов
| Платформа | Техника | Сложность | Производительность |
|---|---|---|---|
| iOS UIKit | UIViewControllerAnimatedTransitioning | Средняя | Высокая |
| iOS SwiftUI | matchedGeometryEffect | Низкая | Высокая |
| Android Compose | AnimatedContent / sharedElement | Средняя | Высокая |
| Flutter | Hero / PageRouteBuilder | Низкая | Средняя |
Кастомные переходы в 2-3 раза снижают когнитивную нагрузку пользователя по сравнению со стандартными, как показали внутренние A/B-тесты.
Типичные грабли и гайдлайны
- Freeze на первом кадре. Если destination view контроллер не завершил layout до начала анимации. Фикс: вызвать
toVC.view.layoutIfNeeded()до старта. - Jumpy status bar.
preferredStatusBarStyleпересчитывается с задержкой. Решение:modalPresentationCapturesStatusBarAppearance = true. - Чёрный прямоугольник под прозрачным NavigationBar.
containerView.backgroundColorне установлен явно. Контейнер наследует.systemBackground, но при анимации opacity могут быть артефакты. - В Flutter Hero требует уникальных тегов. Два Hero с одинаковым
tagломают анимацию.
Согласно Human Interface Guidelines, переходы не должны быть длиннее 400ms для навигации. Для Android Material Design 3 рекомендует 300ms. Modal presentations на iOS (.sheet) системно анимируются снизу вверх — переопределять не стоит.
Рекомендуемые параметры spring-анимаций
| Платформа | Параметр | Значение |
|---|---|---|
| iOS | damping ratio | 0.88 |
| iOS | stiffness | 200 |
| Android | damping ratio | 0.7 |
| Android | stiffness | 300 |
| Flutter | spring mass | 1.0 |
| Flutter | spring stiffness | 100 |
Как мы делаем: процесс и сроки
- Аудит текущих переходов и карта экранов.
- Прототипирование в Xcode/Android Studio/Flutter с подбором spring-параметров.
- Реализация кастомных
UIViewControllerAnimatedTransitioning/NavigationTransition/ Compose transitions /PageRouteBuilder. - Интерактивные жесты там, где уместно.
- Тестирование на реальных устройствах (iPhone SE 2nd gen, Samsung Galaxy A51) через Core Animation Instrument и Android Profiler — гарантируем 60 FPS.
Базовый набор переходов для 3–5 типов экранов занимает 2–3 рабочих дня. Кастомный Hero-transition с интерактивностью — от 3 до 5 дней. Сроки уточняются после аудита проекта. Стоимость разработки одного перехода варьируется от 30 000 до 80 000 рублей в зависимости от сложности. Для стандартного приложения с 10 экранами полный набор обойдётся примерно в 150 000 рублей.
Что входит в работу
- Аудит и карта экранов
- Прототип с подбором анимаций
- Реализация transition-объектов
- Интерактивные жестовые переходы
- Тестирование на реальных устройствах
- Документация и код-ревью
- Поддержка 3 месяца после сдачи
Закажите консультацию
Хотите, чтобы ваше приложение работало с плавными переходами? Закажите консультацию — обсудим проект и предложим решение. Свяжитесь с нами для обсуждения вашего проекта.







