Пришло время заставить эти анимации работать.
С тех пор, как вышел SwiftUI, я стал его большим поклонником. Наконец-то больше никаких раскадровок, никакой дублирующей чепухи, состоящей наполовину из кода, а наполовину в графическом интерфейсе.
Но в последнее время анимация напомнила мне, что в SwiftUI есть еще много неочевидных трюков, которым нужно научиться. Вот три вывода, которые помогут вам начать создавать мощные анимации в SwiftUI.
1. Основы: есть два механизма анимации SwiftUI.
Давайте начнем с простой задачи: на холсте 800x800 вы хотите анимировать круг, перемещающийся из одной точки в другую. Я начну с определения двух состояний для положения круга: xpos
и ypos
. Затем я установлю положение круга с помощью модификатора position
.
Пока все просто — вот наше превью:
Далее я покажу вам оба способа анимации движения. Первый — использовать модификатор animation
. Он прикрепляется после модификатора, который вы хотите анимировать — в данном случае после модификатора положения.
Как часть модификатора анимации вы должны указать значение для отслеживания изменений:
.position(x: xpos, y: ypos) .animation(.easeInOut(duration: 2.0), value: ypos) // Animation follows the position modifier
Здесь мы отслеживаем изменения в ypos
— это означает, что всякий раз, когда вы меняете ypos
, круг будет перемещаться в новую анимированную позицию.
Чтобы вызвать изменение в ypos
, вы можете использовать, например, метод onAppear
:
.onAppear { ypos = 600 // Trigger the animation to start }
Вот полный код и результат:
Одной проблемой при таком подходе является аргумент value: ypos
. Что, если вы хотите анимировать оба изменения в xpos
и ypos
? Кроме того, это немного сбивает с толку — мы должны помнить, что модификатор анимации изменяет предыдущий модификатор (модификатор позиции)… ??? Это может легко привести к ошибкам — если вы поместите модификатор анимации сразу после Circle(), вы заставите его появиться/исчезнуть!
Второй способ сделать анимацию мне более интуитивно понятен — использовать withAnimation
. Вместо модификатора .animation
используем:
withAnimation(.easeInOut(duration: 2.0)) { // withAnimation tells that states modified in closure are animated ypos = 600 }
Блок withAnimation
отслеживает любые изменения State
в замыкании и анимирует их. Это гораздо более чистое решение для меня. Теперь мы также можем свободно менять xpos
в том же блоке, что также будет анимировать положение в горизонтальном направлении.
2. Используйте animableData для более умной анимации по заданным траекториям.
Давайте усложним — давайте анимируем движение маленького синего круга вокруг большого черного круга:
Чтобы нарисовать это, я добавил к ZStack
следующее:
Circle() .strokeBorder(.black, lineWidth: 2) .foregroundColor(.clear) .frame(width: CGFloat(2*radius), height: CGFloat(2*radius)) .position(x: 400, y: 400)
Теперь вместо переменной состояния xpos
и ypos
у нас есть angle
по кругу:
@State var angle: Float = Float.pi / 2.0
а положение меньшего синего круга определяется как:
.position(x: CGFloat(400 + radius * cos(angle)), y: CGFloat(400 - radius * sin(angle)))
В анимации мы пытаемся изменить угол с начального pi/2
на -pi/2
:
.onAppear { withAnimation(.easeInOut(duration: 2.0)) { // withAnimation tells that states modified in closure are animated angle = -Float.pi / 2.0 } }
Давай посмотрим что происходит:
…ну… в любом случае, теория была хороша.
Иди по кругу!
Как мы заставляем его ходить? Со свойством animatableData
.
Во-первых, мы определяем пользовательскую Shape
— анимируемую форму круга с именем CircleWithAnimatableAngle
:
struct CircleWithAnimatableAngle: Shape { var angle: Float }
Функция path
рисует окружность следующим образом:
func path(in rect: CGRect) -> Path { return Path { path in let x = CGFloat(400 + radius * cos(angle)) let y = CGFloat(400 - radius * sin(angle)) path.move(to: CGPoint(x: x, y: y)) path.addEllipse(in: CGRect(x: x - 25.0, y: y - 25.0, width: 50.0, height: 50.0)) } }
Кроме того, мы добавляем свойство animatableData
, которое просто изменяет угол:
var animatableData: Float { get { angle } set { angle = newValue } }
Наконец, мы можем заменить Circle
в виде тела на CircleWithAnimatableAngle
:
CircleWithAnimatableAngle(angle: angle, radius: radius) .foregroundColor(.blue)
Вот полный код и результат:
Ура! Намного лучше.
Но есть очевидная проблема. Теперь мы имеем дело с фигурой — а как насчет любого другого объекта SwiftUI? Как насчет добавленияText
, что мы хотим двигаться вдоль круга?
Нам нужно более общее решение.
3. Общее решение: AnimatableModifier
Это настоящий секрет анимации в SwiftUI: каждая анимация получает свой собственный AnimatableModifier
. Вот AnimatableModifier
для анимации круга:
Мы видим тот же animatableData
, что и раньше. Но теперь вместо метода path
для формы мы реализуем метод body
, который возвращает некое модифицированное представление. В этом случае мы используем угол для обновления положения вида.
Чтобы применить его, мы просто используем модификатор .modifier
:
.modifier(CircleAnimation(angle: angle, radius: radius))
Мы можем добавить этот модификатор к чему угодно — к любому объекту SwiftUI. Затем, когда мы изменим angle
в блоке withAnimation
, модификатор будет управлять тем, как изменение угла преобразуется в изменение положения (или любые другие модификаторы, которые изменяются).
Например, мы можем создать ZStack
, используя как Circle
, так и Text
:
ZStack { Circle() .frame(width: 50, height: 50) .foregroundColor(.blue) Text("Hello!") .offset(x: 80) .font(.system(size: 24)) } .modifier(CircleAnimation(angle: angle, radius: radius))
Вот результат и полный код:
Последние мысли
Мы прошли путь от простых модификаторов .animation
до наследования от протокола AnimatableModifier
. Я действительно предпочитаю этот последний подход, так как он не только более мощный, позволяя вам управлять анимацией, но и более удобочитаемый — какие данные действительно анимируются, четко определено в свойстве animatableData
. Кроме того, его можно использовать повторно — его можно применять к любому объекту SwiftUI для создания согласованных анимаций — что, на мой взгляд, оправдывает дополнительный код.
Спасибо за прочтение!