SA0718 — Анимация и просмотр переходов
Содержание страницы
Разница между хорошим приложением и отличным приложением часто заключается в мелких деталях. Использование правильной анимации в нужных местах может порадовать пользователей и выделить ваше приложение в App Store.
Анимация может сделать ваше приложение более увлекательным и простым в использовании, а также сыграть решающую роль в привлечении внимания пользователя к определенным областям.
Анимация в SwiftUI намного проще, чем анимация в AppKit или UIKit. SwiftUI animations-это абстракции более высокого уровня, которые справляются со всей утомительной работой за вас. Если у вас есть опыт работы с анимацией на платформах Apple, многое из этой главы покажется вам знакомым. Вы обнаружите, что создание анимации в вашем приложении требует гораздо меньше усилий. Вы можете комбинировать или перекрывать анимации и прерывать их без осторожности. Большая часть сложности государственного управления исчезает, когда вы позволяете фреймворку справляться с ней. Это освобождает вас от необходимости создавать отличные анимации вместо того, чтобы обрабатывать крайние случаи и сложности.
В этой главе вы пройдете через процесс добавления анимации в примерный проект. Время, чтобы заставить экран двигаться!
Анимация изменений состояния
Во-первых, откройте начальный проект для этой главы. Создайте и запустите проект для этой главы. Вы увидите приложение, которое показывает информацию о рейсе в аэропорт. Первый вариант отображает табло статуса рейса, которое предоставляет флаерам время и ворота, через которые рейс вылетит или прибудет.

Полетный борт
Примечание: К сожалению, показать анимацию со статическими изображениями в книге довольно сложно. В некоторых случаях вы увидите изображения, на которых используются красные блики, отражающие движение, ожидаемое для некоторых частей этой главы. Вам нужно будет проработать эту главу, используя предварительный просмотр, симулятор или устройство, чтобы лучше понять, как работают анимации. Предварительный просмотр упрощает настройку анимации, но иногда анимация выглядит не совсем правильно в предварительном просмотре. Попробуйте запустить приложение в симуляторе или на устройстве, если вы не видите то же самое в предварительном просмотре, описанном здесь.
Добавление анимации
Для начала откройте FlightInfoPanel.swift в группе FlightDetails и найдите следующий код:
if showTerminal { FlightTerminalMap(flight: flight) }
Этот код переключает отображение карты терминала на основе переменной состояния, showTerminal
. Следующий код непосредственно перед условным условием создает кнопку, переключающую переменную:
Button(action: { showTerminal.toggle() }, label: { HStack(alignment: .center) { Text( showTerminal ? "Hide Terminal Map" : "Show Terminal Map" ) Spacer() Image(systemName: "airplane.circle") .resizable() .frame(width: 30, height: 30) .padding(.trailing, 10) .rotationEffect(.degrees(showTerminal ? 90 : -90)) } })
Этот раздел кода также использует изменение состояния для определения внешнего вида кнопки. Текст изменяется, чтобы отразить действие, которое вызовет следующее нажатие. Вы также изменяете rotationEffect
угол между двумя значениями в зависимости от состояния showTerminal
переменной.
Запустите приложение, и вы увидите, как вращение переключается между двумя состояниями.
Примечание: Если у вас возникли проблемы с анимацией или различиями между анимациями, вы можете включить Debug ▸ Медленная анимация, чтобы значительно снизить скорость анимации. Обязательно выключите его, когда закончите.
Сначала вы добавите анимацию к этому вращению. В SwiftUI вы просто указываете тип анимации и позволяете SwiftUI обрабатывать интерполяцию за вас. После .rotationEffect(_:anchor:)
метода добавьте следующий код:
.animation(.linear(duration: 1.0), value: showTerminal)
В дополнение к типу анимации вы указываете значение, изменение которого запускает анимацию. В более ранних версиях SwiftUI вам не нужно было указывать этот параметр, так как SwiftUI определил бы его. Это позволяло очень легко применять анимационные эффекты случайно. Предыдущий вызов все еще работает, но стал устаревшим в SwiftUI 3.0, что означает, что поддержка исчезнет в будущей версии. Новый код должен содержать это значение, и вы должны обновить любой старый код, чтобы включить его. Обязательно протестируйте свое приложение после этого, так как вы, возможно, полагались на предыдущее поведение.
Запустите приложение, нажмите кнопку «Статус рейса» и выберите любой рейс в списке. По умолчанию карта терминала будет скрыта. Нажмите на текст или значок самолета, чтобы показать карту. Вы увидите, что значок медленно вращается между позициями вверх и вниз, когда вы переключаете вид вместо почти мгновенного изменения по сравнению с предыдущим.

Анимация при вращении изображения
Поворот от -90 до 90 градусов действует как изменение состояния, и вы сказали SwiftUI анимировать это изменение состояния, добавив .animation(_:value:)
модификатор. Анимация применяется только к вращению элемента изображения и никаким другим видам на странице и активируется только при showTerminal
изменении.
Поскольку SwiftUI выполняет итерацию между значениями при анимации, углы имеют значение при создании анимации. Вы можете указать второй угол как 270 градусов, так как оба обеспечивают половину поворота от 90 градусов. Измените второй угол поворота с -90
на 270
. Теперь предварительный просмотр и нажмите на кнопку.
Вы увидите, что шеврон вращается в противоположном направлении от предыдущего. Положительные изменения угла вращаются по часовой стрелке вокруг начала координат, а отрицательные-против часовой стрелки. Раньше шеврон поворачивался по часовой стрелке при движении вверх и указывал вниз. Теперь он вращается против часовой стрелки от 270 до 90 градусов.
Вы не ограничены углом поворота в диапазоне от 0 до 360 градусов одного поворота. Измените значение 270
на 630
(270 плюс полный оборот на 360). Попробуйте приложение сейчас, и вы увидите, что оно вращается полностью и наполовину, прежде чем остановиться. Обратите внимание, что вращение длится столько же времени и ускоряется для компенсации.
Упражнение: Попробуйте использовать другие углы как для начального, так и для конечного угла, чтобы понаблюдать, как разные углы влияют на анимацию и позиции.
Прежде чем продолжить, измените вращение на:
.rotationEffect(.degrees(showTerminal ? 90 : 270))
Типы анимации
До сих пор вы работали с одним типом анимации: линейной анимацией. Это обеспечивает линейное изменение с постоянной скоростью от исходного состояния к конечному. Если бы вы изобразили изменение по вертикали относительно времени по горизонтали, то переход выглядел бы следующим образом:

Линейная анимация
SwiftUI предоставляет еще несколько типов анимации. Различия могут быть тонкими и трудноразличимыми, поэтому вы растянули анимацию до двух секунд. Не все типы анимации принимают параметр длины напрямую, но вы узнаете и другие способы его настройки.
Вы добавите некоторый код, который поможет вам увидеть различия в анимации. Между началом HStack
и Text
представлением внутри кнопки добавьте следующий код:
Image(systemName: "airplane.circle") .resizable() .frame(width: 30, height: 30) .padding(10) .rotationEffect(.degrees(showTerminal ? 90 : 270)) .animation(.linear(duration: 1.0), value: showTerminal) Spacer()
Это изменение добавляет второй значок с текстом по центру между ними. Это дополнение, занимающее половину предыдущего времени анимации, поможет вам сравнить анимации в остальной части этого раздела.

Две иконы
Для второго значка измените animation
метод на:
.animation(.default.speed(0.33), value: showTerminal)
Вы заметите добавление этого speed(_:)
метода. Этот метод является одним из нескольких, которые вы можете применить к любой анимации. Он регулирует скорость анимации, в данном случае замедляя ее, так как значение меньше единицы. Если вы используете значение больше единицы, скорость анимации увеличится.
Запустите приложение и перейдите к деталям полета. Хотя они и не идентичны, вы увидите, что анимация работает с одинаковой скоростью. Без изменения скорости вращение в правой плоскости завершилось бы в три раза быстрее.
Анимация по умолчанию — это тип облегченной анимации, называемый easeInOut
. Эта анимация выглядит хорошо почти во всех случаях, так что это хороший выбор, если у вас нет других сильных предпочтений. Вы изучите различные облегченные анимации в следующем разделе.
Облегченная анимация
Облегченная анимация может быть наиболее распространенной в приложениях. Они обычно выглядят более естественно, так как что-то не может мгновенно изменить скорость в реальном мире. Облегченная анимация применяет ускорение, замедление или и то, и другое в конечных точках анимации. Анимация отражает ускорение или замедление реального движения.
Анимация по умолчанию, которую вы только что использовали, является эквивалентом этого easeInOut
типа. Эта анимация применяет ускорение в начале и замедление в конце анимации.
Если вы изобразили движение в этой анимации в зависимости от времени, то эта анимация выглядит следующим образом:

Легкость входа и выхода
Вы можете получить больше контроля, используя его напрямую. Измените анимацию на втором значке на:
.animation(.easeInOut(duration: 1.0), value: showTerminal)
Облегченная анимация имеет короткое время по умолчанию-0,35 секунды. Вы можете указать другую длину с помощью этого duration:
параметра. Вы использовали это, чтобы установить продолжительность так же, как линейная анимация другого значка.
Запустите приложение, и вы увидите, что две кнопки анимируются одновременно. Нелинейное движение второго также должно быть заметно.
Теперь измените анимацию для второго значка на:
.animation(.easeOut(duration: 1.0), value: showTerminal)
Запустите приложение и переключите карту терминала. Вы увидите, что вращение начинается быстро и замедляется незадолго до остановки.
Графическое отображение движения в этой анимации по времени будет выглядеть следующим образом:

Расслабься
В дополнение к easeOut
этому , вы также можете указатьeaseIn
, что начинается медленно в начале анимации , а затем ускоряется.

Легкость в
Если вам нужен точный контроль над формой анимационной кривой, вы можете использовать этот timingCurve(_:_:_:_)
метод. SwiftUI использует кривую Безье для облегчения анимации. Этот метод позволит вам определить контрольные точки для этой кривой в диапазоне 0…1. Форма кривой будет отражать указанные контрольные точки.

timingCurve
Упражнение: Попробуйте различные облегченные анимации и понаблюдайте за результатами. В частности, посмотрите, что делают различные контрольные точки в
timingCurve(_:_:_:_)
типе анимации.
Весенняя анимация
Облегченные анимации всегда переходят между начальным и конечным состояниями в одном направлении. Они также никогда не проходят ни одно из конечных состояний. Другая категория SwiftUI animations позволяет добавить немного отскока в конце изменения состояния. Физическая модель для этого типа анимации дает ему название: пружина.
Почему весна делает правильную анимацию
Пружины сопротивляются растяжению и сжатию — чем больше растяжение или сжатие пружины, тем большее сопротивление она оказывает. Представьте себе груз, прикрепленный к одному концу пружины. Прикрепите другой конец пружины к неподвижной точке и дайте пружине упасть вертикально с грузом на дне. Он отскочит несколько раз, прежде чем остановится.
В реальном мире трение и другие внешние силы гарантируют, что система теряет энергию каждый раз в течение цикла. Это уменьшение делает систему демпфированной. Эти накопленные потери складываются, и в конце концов вес остановится неподвижно в точке равновесия.
График этого движения выглядит примерно так:

Затухающее простое гармоническое движение
Создание весенней анимации
Измените анимацию для второго значка на:
.animation( .interpolatingSpring( mass: 1, stiffness: 100, damping: 10, initialVelocity: 0 ), value: showTerminal )
Запустите приложение, и вы увидите, что значок теперь немного подпрыгивает в конце, проходя мимо конца и обратно несколько раз, прежде чем остановиться в конечной позиции. Вы увидите, что значок немного проходит мимо пункта назначения, скользит назад, а затем немного подпрыгивает вокруг конечной позиции, прежде чем остановиться.
Параметры, которые вы передаете, те же, что и упомянутые выше:
mass
: Контролирует, как долго система “отскакивает”.stiffness
: Управляет скоростью начального движения.damping
: Управляет скоростью замедления и остановки системы.initialVelocity
: Дает дополнительное начальное движение.
Упражнение: Прежде чем продолжить, посмотрите, можете ли вы определить, как изменения параметров влияют на анимацию.
Совет: экспериментируйте с одним элементом за раз. Сначала удвоьте значение, а затем уменьшите его вдвое по сравнению с исходным значением. Используйте первый значок для сравнения двух анимаций с одним измененным параметром.
Увеличение mass
приводит к тому, что анимация длится дольше и отскакивает дальше по обе стороны конечной точки. Меньшая масса останавливается быстрее и меньше движется мимо конечных точек при каждом отскоке. Увеличение stiffness
приводит к тому, что каждый отскок перемещается дальше конечных точек, но меньше влияет на длину анимации. Увеличение damping
сглаживает и заканчивает его быстрее. Увеличение initialVelocity
приводит к тому, что анимация отскакивает дальше. Негатив initialVelocity
может перемещать анимацию в противоположном направлении до тех пор, пока она не преодолеет начальную скорость.
Если вы не физик, физическая модель анимации интуитивно не соотносится с результатами. SwiftUI представляет более интуитивно понятный способ определения весенней анимации. Базовая модель не меняется, но вы можете указать параметры модели, лучше связанные с тем, как вы хотите, чтобы анимация отображалась в вашем приложении. Измените анимацию на:
.animation( .spring( response: 0.55, dampingFraction: 0.45, blendDuration: 0 ), value: showTerminal )
Он dampingFraction
контролирует, как быстро прекращается “пружинистость”. Нулевое значение никогда не остановится (попробуйте и увидите). Значение единицы или больше приведет к остановке системы без колебаний. Это перегруженное состояние будет выглядеть аналогично ослабленной анимации предыдущего раздела.
Обычно вы используете значение от нуля до единицы, что приведет к некоторому колебанию до окончания анимации. Большие значения замедляются быстрее.
response
Параметр определяет время системы для завершения одного колебания с dampingFraction
установленным нулем. Он позволяет настроить продолжительность анимации.
blendDuration
Параметр предоставляет элемент управления для смешивания длины перехода между различными анимациями. Он используется только в том случае, если вы изменяете параметры во время анимации или объединяете несколько весенних анимаций. Нулевое значение отключает смешивание.
Опять же, попробуйте изменить эти параметры и сравнить созданные анимации.
Удаление и объединение анимаций
Общая проблема в первоначальном выпуске SwiftUI заключалась в том, что анимация иногда могла возникать там, где вы этого не хотели. Добавление value
параметра к animation(_:value:)
адресам во многом решает эту проблему. Все еще могут быть случаи, когда вы захотите не применять анимацию. Вы делаете это, передавая nil``animation(_:value:)
методу тип анимации.
Все еще в FlightInfoPanel.swift добавьте следующий дополнительный модификатор после .rotationEffect
метода:
.scaleEffect(showTerminal ? 1.5 : 1.0)
Это изменение добавляет масштабирование в 1,5 раза по сравнению с исходным размером значка при отображении карты терминала. Если вы просмотрите анимацию, то увидите, что кнопка растет синхронно с вращением. Анимация влияет на все изменения состояния элемента, к которому вы применяете анимацию.
Затем добавьте следующий код между методами rotationEffect()
andscaleEffect()
:
.animation(nil, value: showTerminal)
Снова запустите анимацию. Вы снова должны увидеть почти мгновенный эффект затухания/затухания при вращении значка, но изменение размера по-прежнему показывает весеннюю анимацию. Вы должны думать о animation
влиянии на все изменения состояния, связанные с ним.

Анимация только при одном изменении состояния
Вы можете комбинировать различные анимации, используя .animation(_:value:)
их несколько раз. Измените анимацию на rotationEffect()
от nil
до:
.animation(.linear(duration: 1), value: showTerminal)
Запустите приложение, и вы увидите, что две анимации происходят одновременно, но каждая влияет на другое изменение состояния. Линейная анимация влияет на вращение, в то время как пружина влияет на масштабирование значка. Кроме того, обратите внимание, что SwiftUI аккуратно обрабатывает анимацию разной длины.

Одновременная анимация
Анимация по изменению состояния
До этого момента в этой главе вы применяли анимацию к измененному элементу представления. Вы также можете применить анимацию там, где происходит изменение состояния. При этом анимация применяется ко всем изменениям, которые происходят из-за изменения состояния.
Удалите эти два .animation(_:value:)
метода из изображений. Измените действие кнопки, которая переключает отображение карты терминала на:
withAnimation( .spring( response: 0.55, dampingFraction: 0.45, blendDuration: 0 ) ) { showTerminal.toggle() }
Вы переносите изменение состояния showTerminal
внутрь withAnimation(_:_:)
метода. Этот вызов использует весеннюю анимацию, но вы можете передать этой функции любую анимацию. Запустите приложение, и вы увидите, что два изображения синхронно запускают одну и ту же анимацию.
Использование withAnimation(_:_:)
применяет анимацию к каждому визуальному изменению, возникающему в результате изменения состояния замыкания. Этот метод упрощает код, когда вы хотите использовать одну анимацию для нескольких изменений, возникающих в результате изменения состояния. Будьте осторожны, так как SwiftUI применил анимацию ко всем изменениям состояния, включая неявные, вызванные этим изменением. В этом примере, если другое свойство полагается на showTerminal
значение, анимация также будет применяться к этому свойству.
Теперь, когда вы понимаете основы анимации в SwiftUI, вы будете применять анимацию к другим частям приложения.
Анимация фигур
Откройте DelayBarChart.swift в группе SearchFlights. Это представление содержит гистограмму задержек рейсов, созданную в главе 18: Рисование и пользовательская графика. Вы собираетесь добавить некоторую анимацию к барам, когда они появятся. Во-первых, добавьте переменную состояния в структуру под flight
свойством.
@State private var showBars = 0.0
Теперь измените minuteLength(_:proxy:)
метод на:
func minuteLength(_ minutes: Int, proxy: GeometryProxy) -> CGFloat { let pointsPerMinute = proxy.size.width / minuteRange return CGFloat(abs(minutes)) * pointsPerMinute * showBars }
Последняя строка теперь умножает длину бара на showBars
переменную состояния. Установка showBars
на ноль означает,что полоса не будет отображаться, так как она имеет нулевую длину. Установка showBars
на единицу приведет к отображению полного размера. Теперь добавьте следующий код к первой Rectangle
фигуре внутри модификатора GeometryReader
after theoffset(x:y:)
:
.animation( .easeOut.delay(0.5), value: showBars )
При изменении вы применяете анимацию облегчения по умолчаниюshowBars
. Вы добавляете задержку в полсекунды, чтобы дать время просмотра до начала анимации. Вам нужно вызвать изменение состояния, чтобы начать анимацию. Здесь вы активируете его, когда появится представление. В конце VStack
добавьте следующий код:
.onAppear { showBars = 1.0 }
Код внутри onAppear(perform:)
выполняется, когда на устройстве появляется прикрепленное представление. Здесь вы изменяете значение showBars
на 1.0, которое изменит длину Rectangle
полосы, потому что ширина полосы изменяется благодаря изменению, внесенному в minuteLength(_:proxy:)
Запустите приложение, нажмите Поиск рейсов и выберите первый рейс US 810 в Денвер. Нажмите на Историю времени полета. Вы увидите, что после этой полусекундной паузы появятся полоски.
Обратите внимание, что полосы для ранних рейсов появляются слева и растут к нулевой точке. Это потому, что вы изменили только длину стержня, а не смещение. Чтобы исправить это, измените minuteOffset(_:proxy:)
метод на:
func minuteOffset(_ minutes: Int, proxy: GeometryProxy) -> CGFloat { let pointsPerMinute = proxy.size.width / minuteRange let offset = minutes < 0 ? 15 + minutes * Int(showBars) : 15 return CGFloat(offset) * pointsPerMinute }
Единственное изменение — это расчет offset
. Когда showBars
он един, он действует как прежде. Когда showBars
равно нулю, то смещение бара также становится равным нулю от умножения. Теперь смещение анимируется от нулевой точки до конечной позиции по мере увеличения длины. Визуальный результат заключается в том, что полоса перемещается влево, когда она становится длиннее, заставляя ее появляться из нулевой точки.
Этот delay()
метод также дает вам возможность создавать анимацию для подключения. В следующем разделе вы измените гистограмму, чтобы включить этот эффект.
Каскадная анимация
Этот delay()
метод позволяет указать время в секундах для паузы перед началом анимации. Вы использовали его в предыдущем разделе, чтобы представление было полностью отображено до того, как бары были анимированы.
Вы также можете использовать его, чтобы анимация соединялась в цепочку и давала ощущение прогресса или движения.
Откройте DelayBarChart.swift и перейдите showBars
на:
@State private var showBars = false
Это изменится showBars
на логическое значение. Изменение minuteOffset(_:proxy:)
и minuteLength(_:proxy:)
возврат к исходному коду:
`func minuteLength(_ minutes: Int, proxy: GeometryProxy) -> CGFloat {
let pointsPerMinute = proxy.size.width / minuteRange
return CGFloat(abs(minutes)) * pointsPerMinute
}
func minuteOffset(_ minutes: Int, proxy: GeometryProxy) -> CGFloat {
let pointsPerMinute = proxy.size.width / minuteRange
let offset = minutes < 0 ? 15 + minutes : 15
return CGFloat(offset) * pointsPerMinute
}`
Теперь измените Rectangle
frame
и offset
для бара на:
.frame( width: showBars ? minuteLength(history.timeDifference, proxy: proxy) : 0 ) .offset( x: showBars ? minuteOffset(history.timeDifference, proxy: proxy) : minuteOffset(0, proxy: proxy) )
Вы переместили изменение состояния из метода расчета непосредственно в код представления. Это означает, что вы также можете переместить туда анимацию, как это было в начале главы. Выньте withAnimation(_:_:)
внутреннюю onAppear(perform:)
часть, чтобы она гласила::
.onAppear { showBars = true }
Запустите приложение, чтобы убедиться, что анимация выглядит так же, как и раньше.
Теперь вы можете добавить задержку к анимации. Измените анимацию после смещения наRectangle
:
.animation( .easeInOut.delay(Double(history.day) * 0.1), value: showBars )
Поскольку вы переместили изменение состояния в представление, теперь вы можете применять другую анимацию к каждой итерации ForEach
цикла. Теперь этот код использует day
свойство в качестве счетчика для неуклонного увеличения задержки перед каждым показом анимации. Бар первого дня задерживается на 0,1 секунды. Планка на десятый день задерживается на целую секунду.
Теперь запустите приложение. Вы увидите результат отложенной анимации, когда бары появятся один за другим, обеспечивая немного более визуально захватывающее отображение.
Извлечение анимации из представления
До этого момента вы определяли анимацию непосредственно в представлении. Для изучения и обучения это хорошо работает. Легче поддерживать код в реальных приложениях, когда вы разделяете разные элементы кода. Это также позволяет повторно использовать их. В DelayBarChart.swiftдобавьте следующий код над body
структурой:
func barAnimation(_ barNumber: Int) -> Animation { return .easeInOut.delay(Double(barNumber) * 0.1) }
Вы определяете пользовательский метод анимации так же, как и любое другое свойство или функцию. Он должен возвращать Animation
структуру. Теперь замените animation(_:value:)
в представлении на:
.animation( barAnimation(history.day), value: showBars )
Запустите приложение и убедитесь, что анимация не изменилась. Вы можете повторно использовать эту анимацию в другом месте представления и изменить ее только в одном месте. Для более сложных анимаций извлечение анимации также улучшает читабельность кода.
Затем вы реализуете еще одну сложную анимацию, добавляя визуальный индикатор к карте терминала.
Анимация путей
Запустите приложение и нажмите на статус полета, а затем нажмите на рейс. Переключите карту терминала и обратите внимание на белую линию, отмечающую путь к выходу на рейс. Откройте FlightTerminalMap.swift в группе FlightDetails, и вы увидите, что линия определяется с помощью набора фиксированных точек, масштабированных до размера представления. Приведенный ниже код рисует путь:
Path { path in // 1 let walkingPath = gatePath(proxy) // 2 guard walkingPath.count > 1 else { return } // 3 path.addLines(walkingPath) }.stroke(Color.white, lineWidth: 3.0)
Если вам нужен обзор Path
and GeometryReader
, см. Главу 18: Рисование и пользовательская графика. Вот что делает этот код:
gatePath(_:)
Метод возвращает массивCGPoint
s, масштабированный до текущего представления с использованием функцииGeometryProxy
.- Эта проверка гарантирует, что в массиве есть по крайней мере две точки — достаточно для строки, — а если нет, то возвращает пустой путь.
addLines(_:)
Метод ожидает массив точек. Он перемещает путь к первой точке массива, а затем добавляет линии, соединяющие остальные точки.
Изменение состояния
Чтобы анимировать этот путь, вам нужно изменить состояние свойства, которое SwiftUI знает, как анимировать. Анимация функционирует благодаря Animatable
протоколу. Этот протокол требует реализации animatableData
свойства для описания изменений, происходящих во время анимации.
Вы можете использовать любой тип, для которого реализуется VectorArithmetic
протокол animatableData
. Встроенные реализации для Float
, Double
, и CGFloat
do, и вы использовали их в этой главе.
Shape
SwiftUI имеет методtrim(from:to:)
, который обрезает фигуру на дробную величину, основанную на ее представлении в виде пути. Для фигуры, реализованной в виде контура, этот метод обеспечивает быстрый способ рисования только части контура.
Во-первых, добавьте новую переменную состояния после flight
параметра в верхней части структуры:
@State private var showPath = false
Затем добавьте следующий код после текущей FlightTerminalMap
структуры:
`struct WalkPath: Shape {
var points: [CGPoint]
func path(in rect: CGRect) -> Path {
return Path { path in
guard points.count > 1 else { return }
path.addLines(points)
}
}
}`
Эта структура реализует пользовательское Shape
представление. Вы передаете массив точек и создаете путь, как и раньше.
Вернувшись в FlightTerminalMap
структуру, добавьте новое свойство анимации после свойстваmapname
:
var walkingAnimation: Animation { .linear(duration: 3.0) .repeatForever(autoreverses: false) }
Этот код создает линейную анимацию длиной в три секунды. Этот repeatForever(autoreverses:)
метод устанавливает анимацию на повторение по завершении. Установка autoreverses
на false
, означает, что анимация перезапускается каждый раз, а не перематывается назад перед перезапуском.
Измените закрытие для GeometryReader
использования новой формы, которую вы добавили:
WalkPath(points: gatePath(proxy)) .trim(to: showPath ? 1.0 : 0.0) .stroke(Color.white, lineWidth: 3.0) .animation(walkingAnimation, value: showPath)
Добавленный trim(from:to:)
метод содержит изменение состояния. Вы также прикрепляете анимацию к представлению, сообщая SwiftUI анимировать изменение состояния.
Наконец, добавьте следующий код в конце представления послеoverlay()
:
.onAppear { showPath = true }
Вы используете этот onAppear(perform:)
метод для запуска анимации при появлении представления.
Запустите приложение и покажите любую карту терминала. Вы увидите, как тропинка ведет к воротам, а затем повторяется каждые три секунды.

Анимированный путь к терминалу
Анимация переходов вида
Примечание: Переходы часто отображаются неправильно в предварительном просмотре. Если вы не видите того, что ожидаете, попробуйте запустить приложение в симуляторе или на устройстве.
Первое, что вы должны понять, — это разница между изменением состояния и переходом представления. Изменение состояния происходит при изменении элемента в представлении. Переход включает в себя изменение видимости или присутствия представления.
Откройте FlightInfoPanel.swift и найдите Text
вид между значками в кнопке, которая показывает карту терминала.
Прямо сейчас это выглядит так::
Text( showTerminal ? "Hide Terminal Map" : "Show Terminal Map" )
Этот код показывает изменение состояния. Представление остается прежним, но текст, отображаемый представлением, может измениться. Измените код на:
if showTerminal { Text("Hide Terminal Map") } else { Text("Show Terminal Map") }
Теперь у вас есть переход к представлению. При изменении переменной состояния одно представление заменяется другимshowTerminal
.
Переходы-это специфические анимации, которые возникают при отображении и скрытии представлений. Вы можете подтвердить это, запустив приложение, нажав на статус рейса, а затем на любой рейс. Нажмите кнопку, чтобы показать и скрыть карту терминала несколько раз, и обратите внимание, как вид исчезает и появляется снова. По умолчанию виды включаются и выключаются с экрана, соответственно замирая и затухая.
Многое из того, что вы уже узнали об анимации, работает с переходами. Как и в случае с анимацией, переход по умолчанию-это только одна возможная анимация.
Измените код, отображающий текст кнопки, на:
Group { if showTerminal { Text("Hide Terminal Map") } else { Text("Show Terminal Map") } } .transition(.slide)
Вы используете этот Group
метод для переноса изменения представления. Затем вы применяете переход к группе. Запустите приложение, вернитесь на страницу, и вы увидите — что-то странное. Старый вид ускользает, но не исчезает в течение нескольких секунд. Поскольку переходы-это тип анимации, вы должны использовать withAnimation(_:value:)
функцию вокруг изменения состояния, иначе SwiftUI не покажет указанный переход. Вы уже сделали это, так как действие для кнопки теперь:
Button(action: { withAnimation( .spring( response: 0.55, dampingFraction: 0.45, blendDuration: 0 ) ) { showTerminal.toggle() } }, label: {
В результате SwiftUI применяет как анимацию, так и переход. Вы часто сталкиваетесь с такой проблемой, работая с анимацией и переходами, что делает сохранение анимации с элементом пользовательского интерфейса более управляемым. На данный момент измените кнопку, чтобы использовать withAnimation
метод без типа анимации.
Button(action: { withAnimation { showTerminal.toggle() } }, label: {
В withAnimation(_:value:)
вызове нет анимации. Он не нужен, так как вы устанавливаете его на отдельных элементах представления. Чтобы сохранить анимацию на значках плоскости, добавьте следующий код после каждого Image
просмотра:
.animation( .spring( response: 0.55, dampingFraction: 0.45, blendDuration: 0 ), value: showTerminal )
Запустите приложение и выведите подробную информацию о рейсе. Теперь нажмите, чтобы показать карту терминала, и вы увидите, что вид теперь перемещается с переднего края. Когда вы снова нажмете на кнопку, вы увидите, как вид соскальзывает с задней кромки. Эти переходы обрабатывают случаи,когда направление текста читается для вас справа налево.
Анимация происходит, когда SwiftUI добавляет представление. Фреймворк создает представление и сдвигает его с переднего края. Он также анимирует вид с заднего края и удаляет его, чтобы он больше не занимал ресурсы.
Вы можете создать аналогичный результат с помощью анимации, но вам нужно выполнить эти дополнительные шаги самостоятельно. Встроенные переходы значительно облегчают работу с анимацией просмотра.
Просмотр типов переходов
Тип перехода по умолчанию изменяет непрозрачность представления при его добавлении или удалении. Вид переходит от прозрачного к непрозрачному при вставке и от непрозрачного к прозрачному при удалении. Вы можете создать более индивидуальную версию с помощью .opacity
перехода.
Вы также использовали переход слайда, который вставляет вид с переднего края и удаляет его с заднего края. .move(edge:)
Переход перемещает вид от или к указанному краю при добавлении или удалении. Чтобы увидеть, как вид перемещается вниз и вверх, измените переход на:
.transition(.move(edge: .bottom))
Другие края есть .top
, .leading
и .trailing
.
Помимо перемещения, переходы также могут анимировать представления, появляющиеся на экране. A .scale()
переход приводит к тому, что представление расширяется при вставке из одной точки или сворачивается при удалении в одну точку в центре. При необходимости можно указать параметр масштабного коэффициента для перехода. Масштабный коэффициент определяет отношение размера исходного вида. Нулевая шкала обеспечивает переход по умолчанию в одну точку. Значение меньше единицы приводит к тому, что представление расширяется от этого масштабированного размера при вставке или сворачивается до него при удалении. Значения, превышающие единицу, работают одинаково, за исключением того, что представление в конце перехода больше конечного представления.
Вы также можете указать anchor
параметр для точки вида, в которой происходит переход. Перечисление содержит константы для углов, сторон и центра представления. Вы также можете указать пользовательское смещение.
Окончательный тип перехода позволяет указать смещение либо в виде CGSize, либо в виде пары значений длины. Вид перемещается от этого смещения при вставке и к нему при удалении.
Упражнение: Как и в случае с анимацией, лучший способ увидеть, как работают переходы, — это попробовать их. Возьмите каждый переход и используйте его вместо
.slide
перехода вFlightTerminalMap
свой . Включите и выключите вид и обратите внимание, как работает анимация при появлении и уходе вида.
Извлечение переходов из представления
Вы можете извлечь переходы из представления, как это было с анимацией. Вы добавляете его не на уровне структуры, как в случае с анимацией, а в области файла. В верхней части FlightInfoPanel.swift добавьте следующее:
extension AnyTransition { static var buttonNameTransition: AnyTransition { .slide } }
Это расширение объявляет ваш переход как статическое свойство AnyTransition. Теперь обновите переход по FlightDetails
вызову, чтобы использовать его:
if showTerminal { FlightTerminalMap(flight: flight) .transition(.buttonNameTransition) }
Просмотрите представление и нажмите кнопку, чтобы просмотреть анимацию, и вы увидите, что она работает так же, как и в первом примере перехода.

Переход слайдов
Асинхронные переходы
SwiftUI позволяет указывать различные переходы при добавлении и удалении представления. Измените свойство static на:
extension AnyTransition { static var buttonNameTransition: AnyTransition { let insertion = AnyTransition.move(edge: .trailing) .combined(with: .opacity) let removal = AnyTransition.scale(scale: 0.0) .combined(with: .opacity) return .asymmetric(insertion: insertion, removal: removal) } }
Модификатор используется combined(with:)
для объединения двух переходов. Предварительный просмотр этого нового перехода. Вы увидите, что вид будет двигаться от заднего края по мере его исчезновения. Когда SwiftUI удаляет вид, он сжимается до точки и исчезает.
Теперь, когда вы узнали об анимации и переходах, вы увидите, как связать переходы в более сложные анимации.
Связывание переходов представлений
Второй выпуск SwiftUI добавил множество функций. Тот, который вы будете использовать в этом разделе, — это matchedGeometryEffect
метод. Он позволяет синхронизировать анимацию нескольких представлений. Думайте об этом как о способе сказать SwiftUI соединить анимацию между двумя отдельными объектами.
Откройте AwardsView.swift в группе AwardsView. В этом представлении награды отображаются с использованием сетки, разработанной в главе 16: Сетки. Когда вы нажимаете на награду, она переходит в новое представление, отображающее сведения об этой награде. Вы собираетесь изменить его, чтобы вместо этого всплывающие сведения о премии отображались по сетке.
Добавьте следующий код в верхнюю часть представления после flightNavigation
EnvironmentObject:
@State var selectedAward: AwardInformation?
Когда пользователь нажимает на награду, вы сохраняете ее в этой необязательной переменной состояния. В противном случае имущество будет nil
. Поскольку это действие касания происходит в подпредставлении, вам нужно будет передать его в это подпредставление.
Открыть AwardGrid.swift. Добавьте следующую привязку после awards
свойства:
@Binding var selected: AwardInformation?
Вы передадите состояние от the AwardsView
к theAwardGrid
, используя эту привязку. Измените содержимое ForEach
цикла на:
AwardCardView(award: award) .foregroundColor(.black) .aspectRatio(0.67, contentMode: .fit) .onTapGesture { selected = award }
Вы удалили навигационную ссылку и вместо этого добавили onTapGesture(count:perform:)
метод для установки привязки к выбранной награде. Вам также необходимо обновить предварительный просмотр, чтобы добавить новый параметр привязки. Измените его на:
AwardGrid( title: "Test", awards: AppEnvironment().awardList, selected: .constant(nil) )
Теперь вернитесь в AwardsView.swift и измените представление на:
ZStack { // 1 if let award = selectedAward { // 2 AwardDetails(award: award) .background(Color.white) .shadow(radius: 5.0) .clipShape(RoundedRectangle(cornerRadius: 20.0)) // 3 .onTapGesture { selectedAward = nil } // 4 .navigationTitle(award.title) } else { ScrollView { LazyVGrid(columns: awardColumns) { AwardGrid( title: "Awarded", awards: activeAwards, selected: $selectedAward ) AwardGrid( title: "Not Awarded", awards: inactiveAwards, selected: $selectedAward ) } } .navigationTitle("Your Awards") } }
Теперь у вас есть aZStack
, который показывает одно из двух представлений в зависимости от результатов if
оператора. Код внутри условия else не изменился, кроме передачи привязки к переменной selectedAward
state. Есть некоторые изменения, которые стоит отметить:
- Первое изменение заключается в том, что вы пытаетесь развернуть переменную состояния
selectedAward
state. Если это не удается, вы показываете сетку, как и раньше, в тойelse
части инструкции. - Если разворачивание прошло успешно, вы отобразите
AwardDetails
представление, которое ранее былоNavigationLink
целью. - Вы снова устанавливаете
selectedAward
переменную состоянияnil
, когда пользователь нажимает на представление. Это изменение удаляетAwardDetails
представление и отображает сетку. - Вы устанавливаете заголовок на название текущей награды
Запустите приложение. Нажмите на свои награды, а затем на любую награду в сетке. Вы увидите, как вид переключается на большой дисплей сведений о награде. Нажмите на AwardDetails
вид, и сетка появится снова.
Переход происходит резко. Вы знаете, что можете исправить это, добавив переход вида. Найдите onTapGesture
метод в AwardsView
(под третьим комментарием) и измените его на:
.onTapGesture { withAnimation { selectedAward = nil } }
Как уже упоминалось в этой главе, это говорит SwiftUI анимировать события, вызванные изменением состояния закрытия. Для другого конца перехода найдите onTapGesture
метод in AwardGrid
и измените его на:
.onTapGesture { withAnimation { selected = award } }
Запустите приложение, и вы обнаружите, что изменения работают лучше. Теперь у вас есть хороший эффект затухания/затухания, который сглаживает ранее резкие переходы между видами. По-прежнему нет смысла связывать эти изменения. Два представления, между которыми вы переходите, разделены и не связаны между собой. Вот тутmatchedGeometryEffect(id:in:properties:anchor:isSource:)
-то все и начинается. Он позволяет соединить два перехода вида.
Вы должны указать только первые два параметра. Это id
работает так же, как и другие id
s, с которыми вы сталкивались в SwiftUI. Он однозначно идентифицирует соединение, поэтому дает двум элементам одинаковые id
ссылки на их анимацию. Вы передаете а Namespace
в in
собственность. Пространство имен группирует связанные элементы, и они вместе определяют уникальные связи между представлениями.
Создать пространство имен очень просто. В верхней части AwardsView
добавьте следующий код после переменной selectedAward
состояния.
@Namespace var cardNamespace
Теперь у вас есть уникальное пространство имен для этого метода. Теперь, после onTapGesture
прилагаемого метода AwardDetails
, добавьте следующее:
.matchedGeometryEffect( id: award.hashValue, in: cardNamespace, anchor: .topLeading )
Вы используете существующее hashValue
свойство в качестве идентификатора вместе с созданным пространством имен. Вы можете использовать любой идентификатор, если он уникален в пространстве имен и согласован. Вы также указываете anchor
параметр для указания местоположения в представлении, используемом для создания общих значений. Это не всегда необходимо, но в данном случае это улучшает анимацию.
Теперь у вас есть одна сторона, но вам нужно связать изменение состояния в подвид. Для этого вам нужно передать пространство имен в это представление. Измените AwardGrid
s внутриLazyVGrid
, чтобы добавить его в качестве параметра:
AwardGrid( title: "Awarded", awards: activeAwards, selected: $selectedAward, namespace: cardNamespace ) AwardGrid( title: "Not Awarded", awards: inactiveAwards, selected: $selectedAward, namespace: cardNamespace )
Теперь откройте AwardGrid. Во-первых, вам нужно добавить свойство для захвата переданного пространства имен. После selected
привязки добавьте следующий код:
var namespace: Namespace.ID
Когда вы передаете пространство имен в представление, оно передается как Namespace.ID
тип.
Вам также необходимо обновить до предварительного просмотра, чтобы передать этот новый параметр. Добавьте следующее в верхнюю часть AwardGrid_Previews
структуры:
@Namespace static var namespace
Обновите представление до:
AwardGrid( title: "Test", awards: AppEnvironment().awardList, selected: .constant(nil), namespace: namespace )
Теперь добавьте следующий код после onTapGesture(count:perform:)
вызова:
.matchedGeometryEffect( id: award.hashValue, in: namespace, anchor: .topLeading )
Обратите внимание, что это использует пространство имен, которое вы передали, и поэтому является тем же пространством имен, что и родительское представление. Вы также используете hashValue
свойство в награде, опять же то же самое, что используется в родительском представлении. При совпадении двух параметров SwiftUI знает, как связать переходы.
Запустите приложение прямо сейчас. Когда вы нажимаете на награду в маленькой сетке, она сдвигается и расширяется при переходе к AwardDetails
виду. Точно так же, когда вы касаетесь AwardDetails
вида, он сжимается и возвращается к меньшему виду внутри сетки.
Добавление matchedGeometryEffect()
упорядочивает только геометрию связанных видов. Обычные механизмы перехода, применяемые к представлениям, все еще действуют во время перехода.
Создание анимации холста
В главе 18″ Рисование и пользовательская графика» вы узнали о Canvas
представлении, предназначенном для повышения производительности сложного чертежа, главным образом когда он использует динамические данные. В сочетании с TimelineView
тем, что вы использовали в главе 15: Расширенные списки, он обеспечивает платформу для создания анимированных рисунков. В этом разделе вы создадите простую анимацию самолета для начального просмотра приложения.
Создайте новое представление SwiftUI с именем WelcomeAnimation. В верхней части нового представления добавьте следующие два свойства:
private var startTime = Date() private let animationLength = 5.0
startTime
Свойство будет содержать время появления представления и будет использоваться для определения продолжительности работы анимации. animationLength
Свойство будет определять, сколько времени потребуется для завершения анимации.
Затем замените текущее тело представления наTimelineView
:
TimelineView(.animation) { timelineContext in }
Вы указываете .animation
расписание, требующее от SwiftUI обновления как можно быстрее. Внутри TimelineView
закрытия добавьте следующий код:
Canvas { graphicContext, size in // 1 let timePosition = (timelineContext.date.timeIntervalSince(startTime)) .truncatingRemainder(dividingBy: animationLength) // 2 let xPosition = timePosition / animationLength * size.width // 3 graphicContext.draw( Text("*"), at: .init(x: xPosition, y: size.height / 2.0) ) } // Extension Point
Canvas
Представление расширяется, чтобы заполнить родительское представление. Вы используете параметр graphicContext
for drawing, и этот size
параметр дает вам размеры пространства чертежа. Затем вы выполните некоторые вычисления для выполнения анимации.
- Сначала вы получаете разницу в секундах между датой от
timelineContext
параметра до закрытия и временем загрузки представления вstartTime
свойство. Вы используетеtruncatingRemainder(dividingBy:)
метод для результирующего Double, чтобы ограничить это значение диапазона от нуля доanimationLength
свойства представления. Когда значение достигнетanimationLength
, оно обернется вокруг нуля. - Вы делите значение с первого шага на
animationLength
свойство, чтобы получить долю всей длины анимации, которую представляет время. Вы умножаете эту дробь на ширину холста, давая горизонтальное положение для этого кадра анимации. - На данный момент вы просто напишете звездочку в горизонтальном положении со второго шага и вертикальном положении по центру холста.
Чтобы увидеть, что вы сделали до этого момента, вернитесь в WelcomeView.swift. Добавьте следующий код в началоScrollView
:
WelcomeAnimation() .foregroundColor(.white) .frame(height: 40) .padding()
Запустите приложение, и вы увидите, что ваша анимация работает, так как маленькая белая звездочка будет скользить над кнопками на экране.
Анимация выглядит неплохо, но теперь нам нужно добавить самолет. Создание самолета кажется пустой тратой времени, когда SF Symbols предоставляет идеально подходящее изображение самолета. К счастью, SwiftUI позволяет переносить внешние представления SwiftUI в холст для использования. Вернитесь в WelcomeAnimation.swift. Расширьте текущее Canvas
представление следующим дополнительным закрытием вместо // Extension Point
комментария.
symbols: { Image(systemName: "airplane") .resizable() .aspectRatio(1.0, contentMode: .fit) .frame(height: 40) .tag(0) }
Убедитесь, что код начинается сразу после закрывающей скобки текущего закрытия в той же строке. symbols
Параметр для Canvas
создает ViewBuilder для предоставления представлений SwiftUI на холст. Здесь вы предоставляете вид изображения с модификаторами для создания квадратного изображения с 40 точками. Каждому представлению внутри symbols
замыкания должно быть присвоено уникальное значение с помощью tag(_:)
модификатора.
Теперь вы можете использовать это переданное представление SwiftUI внутри Canvas
. В верхней части Canvas
закрытия добавьте следующую строку:
guard let planeSymbol = graphicContext.resolveSymbol(id: 0) else { return }
Вы используете контекст resolveSymbol(id:)
on the graphics для доступа к представлениям SwiftUI. id
Здесь должен соответствовать идентификатору, указанному в tag(_:)
модификаторе представления. Если символ не существует, вы возвращаетесь, так как рисовать нечего, в результате чего получается пустой холст. Теперь измените существующий GraphicsContext.draw(_:at:anchor:)
метод (после третьего комментария) на:
graphicContext.draw( planeSymbol, at: .init(x: xPosition, y: size.height / 2.0) )
Вместо текста теперь вы рисуете представление SwiftUI, используя тот же draw(_:at:anchor:)
метод, передавая planeSymbol
полученный с помощью resolveSymbol(id:)
. Запустите приложение, чтобы увидеть готовую анимацию.

Анимированный самолет холст
Ключевые моменты
- Не используйте анимацию только ради этого. У каждой анимации есть своя цель.
- Продолжительность анимации должна составлять от 0,25 до 1,0 секунды. Более короткие анимации часто не заметны. Более длинные анимации могут раздражать вашего пользователя, желающего что-то сделать.
- Поддерживайте согласованность анимации в приложении и с использованием платформы.
- Анимация должна быть необязательной. Соблюдайте настройки специальных возможностей, чтобы уменьшить или устранить анимацию приложения.
- Убедитесь, что анимация плавная и переходит из одного состояния в другое.
- Анимация может иметь огромное значение в приложении, если ее использовать с умом.
- Использование
matchedGeometryEffect
позволяет связать переходы просмотра в одну анимацию. - Вы можете создавать высокопроизводительные анимации, комбинируя
TimelineView
иCanvas
.
Куда идти дальше?
В этой главе речь шла о создании анимации и переходов, но не о том, зачем и когда их использовать. Хорошей отправной точкой для вопросов, связанных с пользовательским интерфейсом на платформах Apple, являются Рекомендации по человеческому интерфейсу: https://developer.apple.com/design/human-interface-guidelines/.
Сессия WWDC 2018, посвященная проектированию гибких интерфейсов, также подробно описывает жесты и движения в приложениях. Вы можете посмотреть его по адресу https://developer.apple.com/videos/play/wwdc2018/803.