Мы собрали 20 лет заметок о световой живописи в один гид. Он бесплатный. Читать гид ×
× ПриложенияТехнологииГалерея БлогО студииНаписать нам ›
Главная/Блог/Анимация в iOS с SwiftUI: плавные переходы для творческих приложений
Технологии

Анимация в iOS с SwiftUI: плавные переходы для творческих приложений

Андрей Морозов · 2026-10-12 · oodot

Анимация в SwiftUI — не украшение интерфейса, а способ объяснить пользователю, что именно произошло на экране. В творческом приложении, где движение и свет — часть продукта, она становится частью языка, на котором интерфейс разговаривает с человеком. Разберём, какие инструменты SwiftUI для этого даёт и как не перегрузить ими интерфейс.

TL;DR: Implicit-анимация (.animation()) подходит для простых локальных эффектов, explicit (withAnimation) — для управляемых переходов между несколькими элементами. Spring-кривые делают движение естественным, потому что ведут себя как объект с массой, а не как механический таймер. Не анимируйте всё подряд: анимация должна объяснять изменение, а не заполнять паузу, и обязана уважать настройку Reduce Motion.
Руки держат iPhone в воздухе, по экрану течёт светящаяся анимационная линия, тёмный фон

Чем implicit-анимация отличается от explicit

SwiftUI предлагает два принципиально разных способа запустить анимацию. Implicit — модификатор .animation(), повешенный на саму вью: любое изменение значения, от которого зависит её вид (цвет, масштаб, смещение), SwiftUI анимирует автоматически, без дополнительного кода. Explicit — вызов withAnimation { } вокруг изменения состояния: анимируется не одна вью, а всё, что зависит от этого состояния в любом месте дерева интерфейса.

Этот выбор — не только техническая деталь: он задаёт архитектуру анимаций на весь проект, и разумно продумывать её ещё на этапе прототипа вместе со специалистами по UI/UX-дизайну интерфейсов, а не добавлять анимацию поверх готового макета. Implicit хорош для точечных эффектов на одной вью, explicit — для согласованных переходов между несколькими.

// Implicit: анимация привязана к самой вью
Circle()
    .scaleEffect(isOn ? 1.4 : 1.0)
    .animation(.easeInOut(duration: 0.3), value: isOn)

// Explicit: анимация управляет изменением состояния
Button("Переключить") {
    withAnimation(.spring()) {
        isOn.toggle()
    }
}

На практике explicit выбирают чаще: он даёт полный контроль над тем, что именно анимируется и когда, а implicit — там, где эффект локальный и не требует синхронизации с другими элементами экрана.

Тайминг-кривые: easeIn, easeOut, easeInOut и когда они ощущаются «правильно»

Кривая тайминга определяет, как меняется скорость движения внутри анимации, а не только её общую длительность. .easeIn стартует медленно и ускоряется к концу — ощущается как «разгон», уместна для элементов, уходящих с экрана. .easeOut стартует быстро и замедляется — естественна для элементов, появляющихся и «оседающих» на месте. .easeInOut комбинирует оба эффекта и чаще всего звучит наиболее нейтрально для переходов между состояниями.

С iOS 17 Apple добавила три именованные кривые, которые описывают не математику, а ощущение: .smooth — плавная и предсказуемая, .snappy — быстрая реакция с лёгким перехлёстом, .bouncy — выраженный упругий отскок. Это удобный способ подобрать тайминг «на слух», не вычисляя параметры кривой Безье вручную — но важно помнить, что эти три значения доступны только на iOS 17 и новее, и на iOS 16 их придётся заменить явным .spring() с похожими параметрами.

Spring-анимация: почему интерфейс должен вести себя как объект с массой

Линейная анимация движется с постоянной скоростью — технически корректно, но визуально мертво: в реальном мире ничто не стартует и не останавливается рывком. Spring-анимация в SwiftUI имитирует физику пружины: у объекта есть масса, жёсткость пружины и демпфирование, которые вместе определяют, как быстро и с каким перехлёстом он придёт в конечное состояние.

Четыре параметра стоит понимать на бытовом уровне, а не как формулы:

  • mass — «вес» анимируемого объекта: чем он больше, тем инертнее реагирует на импульс;
  • stiffness — жёсткость пружины: выше значение — быстрее объект стремится к цели;
  • damping — сопротивление движению: низкое значение даёт заметный отскок, высокое — гасит его почти до нуля;
  • initialVelocity — стартовая скорость движения, полезна, когда анимация продолжает жест пользователя (например, после свайпа).

Для творческого инструмента — рисования, светового следа, жестовых переходов — spring почти всегда выигрывает у линейной анимации: пользователь ведёт пальцем или светом естественно, и интерфейс должен отвечать так же, а не механическим шагом с фиксированной скоростью.

Переходы между состояниями и экранами

Модификатор .transition() определяет, как вью появляется и исчезает при добавлении или удалении из дерева интерфейса — в отличие от .animation(), который анимирует изменение уже существующей вью.

ПереходКогда использовать
.scaleЭлемент «вырастает» или «схлопывается» — хорошо для карточек и всплывающих панелей
.opacityМягкое появление/исчезание без движения — нейтральный выбор по умолчанию
.offset / .moveЭлемент въезжает или выезжает по одной оси — уместно для панелей и уведомлений
.slideГотовая комбинация смещения и прозрачности для списков
.asymmetricПоявление и исчезание выглядят по-разному — например, масштаб на входе и прозрачность на выходе

Для переходов между экранами или крупными состояниями — например, когда карточка в списке «превращается» в полноэкранную деталь — в SwiftUI есть matchedGeometryEffect. Он связывает геометрию двух разных вью общим идентификатором, и система сама анимирует переход формы одной в форму другой. Это не инструмент для рядовых переходов, а точечное решение для «геройских» эффектов, когда важно сохранить визуальную непрерывность элемента между экранами.

Почему анимация в SwiftUI может лагать?

Чаще всего причина не в самой анимации, а в том, что она запускает вокруг себя. Три типичных источника просадки кадров:

  • Сложная View-иерархия под анимируемым элементом — если анимация провоцирует пересчёт layout у десятков вложенных вью, система тратит кадровый бюджет не на отрисовку, а на раскладку;
  • Тяжёлые эффекты поверх анимации — блюр, тени с большим радиусом или кастомные шейдеры, применённые к анимируемой вью, умножают стоимость каждого кадра;
  • Анимация, завязанная на дорогие вычисления в body — если вычисление, вызываемое при каждом обновлении состояния, само по себе тяжёлое, анимация унаследует эту задержку.

Быстрый способ это диагностировать — включить slow-motion анимаций в симуляторе (Debug menu → Slow Animations, либо сочетание клавиш в Xcode) и смотреть на рывки и подёргивания в замедленном темпе: на нормальной скорости глаз их часто не замечает, а в замедлении источник проблемы виден сразу. Для точных цифр по кадрам дальше стоит профилировать Core Animation в Instruments — и обязательно на самом младшем из поддерживаемых устройств, а не только на новом iPhone из тестового парка.

Доступность: Reduce Motion и когда анимацию нужно выключать

iOS даёт пользователю системную настройку Reduce Motion, и в SwiftUI её можно прочитать через @Environment(\.accessibilityReduceMotion). Логика проста: если настройка включена, замените выразительную анимацию (масштабирование, параллакс, крупные перемещения) на статичный переход или простое изменение прозрачности.

@Environment(\.accessibilityReduceMotion) var reduceMotion

var body: some View {
    content
        .animation(reduceMotion ? .none : .spring(), value: isOn)
}

Для творческого приложения это не опциональная деталь. Часть аудитории включает Reduce Motion не из-за вестибулярной чувствительности, а просто предпочитая спокойный интерфейс или экономя батарею — и продукт, который игнорирует этот сигнал, выглядит менее аккуратным вне зависимости от того, насколько красива сама анимация.

Практический пример: 5 шагов — анимируем появление мазка света в творческом UI

  1. Опишите состояние. Заведите @State private var strokeVisible = false — булево значение, которое включает появление светового следа на экране.
  2. Соберите форму следа. Нарисуйте линию или градиентную полосу через Path или готовую фигуру SwiftUI, не привязывая к ней анимацию на этом шаге.
  3. Добавьте implicit-анимацию прозрачности и масштаба. Повесьте на форму .opacity(strokeVisible ? 1 : 0), .scaleEffect(strokeVisible ? 1 : 0.85) и .animation(.spring(response: 0.45, dampingFraction: 0.7), value: strokeVisible) — spring даёт следу лёгкий «живой» отклик вместо механического появления.
  4. Запустите изменение через explicit-анимацию. В обработчике жеста оберните strokeVisible = true в withAnimation, если появление следа должно синхронизироваться с другими элементами экрана — например, одновременным затемнением фона.
  5. Проверьте Reduce Motion и производительность. Добавьте проверку accessibilityReduceMotion, замените spring на мягкое появление прозрачностью при включённой настройке, и прогоните slow-motion в симуляторе на младшем поддерживаемом устройстве, прежде чем считать эффект готовым.

Частые вопросы об анимации SwiftUI

В чём разница между .animation() и withAnimation?

.animation() — implicit-анимация на уровне конкретной вью: SwiftUI сам анимирует изменение заданных свойств. withAnimation { } — explicit-анимация вокруг изменения состояния: под анимацию попадает всё, что от него зависит, в любом месте дерева вью. Implicit проще для локального эффекта, explicit надёжнее для согласованных переходов.

Какую длительность анимации выбрать по умолчанию?

Для большинства UI-переходов комфортный диапазон — 0.25–0.4 секунды. Для крупных эффектных переходов можно растянуть до 0.6–0.8 секунды. Дольше 1 секунды уместно редко — это обычно признак того, что анимация стала самоцелью.

Можно ли анимировать появление и исчезание по-разному?

Да, через .transition(.asymmetric(insertion:, removal:)) — элемент может появляться одним способом и пропадать другим, чтобы пара переходов не выглядела механическим реверсом самой себя.

Что такое matchedGeometryEffect и когда он нужен?

Это модификатор, который связывает геометрию двух разных вью общим идентификатором namespace и анимирует переход формы одной в форму другой — классический hero-переход. Нужен именно для «геройских» переходов между экранами, а не для рядовых локальных анимаций.

Как проверить, что анимация не вызывает лаги на старых устройствах?

Включите slow-motion анимаций в симуляторе и визуально отследите рывки кадра, затем профилируйте Core Animation в Instruments на самом младшем из поддерживаемых устройств — расхождение в производительности с новым iPhone может быть кратным.

Нужно ли поддерживать Reduce Motion, если приложение не для всех аудиторий?

Да, в любом случае. Эту системную настройку включают не только пользователи с вестибулярной чувствительностью, но и те, кто предпочитает спокойный интерфейс или бережёт батарею. Игнорировать её — значит оттолкнуть часть аудитории вне зависимости от ниши.

Работают ли примеры из статьи в iOS 16, или нужен iOS 17+?

Implicit и explicit анимации, spring через .spring() и .transition() работают в iOS 16 без изменений. Именованные кривые .smooth, .snappy и .bouncy — API iOS 17+; на более старых системах их заменяет явный .spring(response:dampingFraction:) с похожими параметрами.

Анимация в SwiftUI — это язык, которым интерфейс объясняет изменения, а не декоративный слой поверх готового UI. Implicit и explicit дают разный уровень контроля, spring делает движение естественным, а два правила — не анимировать всё подряд и уважать Reduce Motion — отделяют продуманный творческий интерфейс от набора эффектов ради эффектов.

Есть вопрос по теме?

Напишите нам — с удовольствием разберём ваш случай и, может быть, сделаем из него следующую статью.

xdt_software@oodot.com ›