Пришло время заставить эти анимации работать.

С тех пор, как вышел 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 для создания согласованных анимаций — что, на мой взгляд, оправдывает дополнительный код.

Спасибо за прочтение!