ссылка на 7ю часть

Базовые Анимации и перемещения

SwiftUI предоставляет возможность анимировать изменения для отдельных представлений и переходы между представлениями. Вы можете использовать встроенные анимации для создания различных эффектов и разумеется создавать кастомные. В этой части мы изучим неявные и явные анимации в SwiftUI, а также создадим несколько демонстрационных проектов для практики.

Явные и неявные Анимации

SwiftUI предоставляет два типа анимаций: неявные и явные. Оба подхода позволяют анимировать представления и переходы между представлениями. Для реализации неявных анимаций используется модификатор animation, который присоединяется к анимируемым представлениям. Вы можете указать предпочитаемый тип анимации, а также необязательно задать продолжительность и задержку. SwiftUI автоматически воспроизведет анимацию на основе изменений состояния представлений.

Явные анимации предоставляют более точный контроль над анимациями. Вместо присоединения модификатора к представлению, вы указываете в блоке withAnimation(), какие изменения состояния вы хотите анимировать.

Если это выглядит для вас немного спутанно, не беспокойтесь. После просмотра нескольких примеров у вас будет более четкое понимание.

Неявные анимации

Давайте начнем с неявной анимации. Создайте новый проект, назовите его SwiftUIAnimation (или любое другое имя, которое вам нравится). Обязательно выберите SwiftUI для интерфейса!

Рассмотрите скриншоты. 

Здесь мы видим простое нажатие на вью, которое состоит из красного круга и сердца. Когда пользователь нажимает на сердце или круг, цвет круга меняется на светло-серый, а цвет сердца - на красный. В то же время размер сердца увеличивается. Здесь происходят различные изменения состояния:

  1. Цвет круга меняется с красного на светло-серый.

  2. Цвет сердца меняется с белого на красный.

  3. Размер сердца увеличивается в два раза по сравнению с оригинальным.

Чтобы реализовать нажатие на круг с использованием SwiftUI, добавьте следующий код в ContentView.swift:

struct ContentView: View {
	@State private var circleColorChanged = false
	@State private var heartColorChanged = false
	@State private var heartSizeChanged = false
	var body: some View {
		ZStack {
			Circle()
				.frame(width: 200, height: 200)
				.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
			Image(systemName: "heart.fill")
				.foregroundColor(heartColorChanged ? .red : .white)
				.font(.system(size: 100))
				.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
		}
		.onTapGesture {
			circleColorChanged.toggle()
			heartColorChanged.toggle()
			heartSizeChanged.toggle()
		}
	}
}

Для моделирования состояний цвета круга, цвета сердца и размера сердца, мы определяем три переменные состояния с начальными значениями false. Чтобы создать круг и сердце, мы используем ZStack для наложения изображения сердца на круг. SwiftUI предоставляет модификатор onTapGesture, который позволяет обнаружить жест нажатия. Вы можете присоединить его к любому вью, чтобы сделать его нажимаемым. В блоке кода onTapGesture мы меняем состояния, чтобы изменить внешний вид представления.

В канвасе для предварительного просмотра нажмите на вью с сердцем. Цвет круга и сердца должен измениться соответственно. Однако, эти изменения не анимированы.

Чтобы анимировать изменения, вам необходимо применить модификатор анимации к представлениям Circle и Image:

struct ContentView: View {
	@State private var circleColorChanged = false
	@State private var heartColorChanged = false
	@State private var heartSizeChanged = false
	var body: some View {
		ZStack {
			Circle()
				.frame(width: 200, height: 200)
				.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
				.animation(.default, value: circleColorChanged)
			Image(systemName: "heart.fill")
				.foregroundColor(heartColorChanged ? .red : .white)
				.font(.system(size: 100))
				.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
				.animation(.default, value: heartSizeChanged)
		}
		.onTapGesture {
			circleColorChanged.toggle()
			heartColorChanged.toggle()
			heartSizeChanged.toggle()
		}
	}
}

SwiftUI отслеживает изменение значения, которое вы указываете в модификаторе анимации. Когда происходит изменение, он вычисляет и воспроизводит анимацию, которая позволяет представлениям плавно переходить из одного состояния в другое. Нажмите на сердце еще раз и вы увидите плавную анимацию.

Вы можете применить модификатор анимации не только к одному представлению, но и к группе представлений. Например, вы можете переписать код выше, присоединив модификатор анимации к ZStack следующим образом:

struct ContentView: View {
	@State private var circleColorChanged = false
	@State private var heartColorChanged = false
	@State private var heartSizeChanged = false
	var body: some View {
		ZStack {
			Circle()
				.frame(width: 200, height: 200)
				.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
			Image(systemName: "heart.fill")
				.foregroundColor(heartColorChanged ? .red : .white)
				.font(.system(size: 100))
				.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
		}
		.animation(.default, value: circleColorChanged)
		.animation(.default, value: heartSizeChanged)
		.onTapGesture {
			circleColorChanged.toggle()
			heartColorChanged.toggle()
			heartSizeChanged.toggle()
		}
	}
}

Это работает аналогичным образом. SwiftUI ищет все изменения состояния, вложенные в ZStack, и создает анимации. В примере мы используем анимацию по умолчанию. SwiftUI предоставляет ряд встроенных анимаций для выбора, включая линейную, плавную в начале, плавную в конце, плавную в начале и конце, а также пружинную. Анимация linear анимирует изменения с линейной скоростью, в то время как другие анимации имеют различную скорость. Для получения подробной информации вы можете посетить сайт www.easings.net, чтобы увидеть разницу между каждой из функций ease.

Чтобы использовать альтернативную анимацию, вам просто нужно установить конкретную анимацию в модификаторе анимации. Допустим, вы хотите использовать анимацию spring, вы можете изменить .default на следующее:

struct ContentView: View {
	@State private var circleColorChanged = false
	@State private var heartColorChanged = false
	@State private var heartSizeChanged = false
	var body: some View {
		ZStack {
			Circle()
				.frame(width: 200, height: 200)
				.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
			Image(systemName: "heart.fill")
				.foregroundColor(heartColorChanged ? .red : .white)
				.font(.system(size: 100))
				.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
		}
		.animation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3),
				   value: circleColorChanged)
		.animation(.default, value: heartSizeChanged)
		.onTapGesture {
			circleColorChanged.toggle()
			heartColorChanged.toggle()
			heartSizeChanged.toggle()
		}
	}
}

Это создает анимацию на основе пружины, которая придает сердцу эффект подергивания. Вы можете регулировать значения затухания и смешивания, чтобы добиться другого эффекта.

Явная анимация

Итак мы разобрались как анимировать вью с помощью неявной анимации. Теперь давайте посмотрим, как можно добиться того же результата, используя явную анимацию. Как объяснялось ранее, вам нужно заключить изменения состояния в блок withAnimation.

Чтобы создать тот же анимированный эффект, вы можете написать код следующим образом:

struct ContentView: View {
	@State private var circleColorChanged = false
	@State private var heartColorChanged = false
	@State private var heartSizeChanged = false
	var body: some View {
		ZStack {
			Circle()
		.frame(width: 200, height: 200) .foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
			Image(systemName: "heart.fill")
				.foregroundColor(heartColorChanged ? .red : .white)
				.font(.system(size: 100))
				.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
		}
		.onTapGesture {
			withAnimation(.default) {
				self.circleColorChanged.toggle()
				self.heartColorChanged.toggle()
				self.heartSizeChanged.toggle()
			}
		}
	}
}

Мы больше не используем модификатор анимации, вместо этого мы заключаем код в onTapGesture с помощью withAnimation. 

Вызов withAnimation принимает параметр анимации. Здесь мы указываем использовать анимацию по умолчанию. Когда вы хотите изменить анимацию на spring, вы можете обновить withAnimation следующим образом:

		.onTapGesture {
			withAnimation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3)) {
				self.circleColorChanged.toggle()
				self.heartColorChanged.toggle()
			}
			self.heartSizeChanged.toggle()
		}

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

struct ContentView: View {
	@State private var circleColorChanged = false
	@State private var heartColorChanged = false
	@State private var heartSizeChanged = false
	var body: some View {
		ZStack {
			Circle()
				.frame(width: 200, height: 200)
				.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
				.animation(.spring(response: 0.3,
								   dampingFraction: 0.3,
								   blendDuration: 0.3),
						   value: circleColorChanged)
			Image(systemName: "heart.fill")
				.foregroundColor(heartColorChanged ? .red : .white)
				.font(.system(size: 100))
				.animation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3
								  ),
						   value: heartColorChanged)
				.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
		}
		.onTapGesture {
			self.circleColorChanged.toggle()
			self.heartColorChanged.toggle()
			self.heartSizeChanged.toggle()
		}
	}
}

Для представления Image мы размещаем модификатор анимации прямо перед scaleEffect. Это отменит анимацию. Изменение состояния модификатора scaleEffect не будет анимировано. Хотя вы можете создать ту же анимацию с помощью неявной анимации, на мой взгляд, в этом случае удобнее использовать явную анимацию.

Создание индикатора загрузки с помощью RotationEffect

Сила анимации SwiftUI заключается в том, что вам не нужно заботиться о том, как анимировать представления. Вам просто нужно предоставить начальное и конечное состояние. SwiftUI автоматически определит остальное. Используя эту концепцию, вы можете создавать различные типы анимаций.

Например, давайте создадим простой индикатор загрузки, который можно обычно найти в реальных приложениях. Чтобы создать индикатор загрузки, как показано на скриншоте, мы начнем с незамкнутого круга:

struct ContentView: View {
	var body: some View {
		Circle()
			.trim(from: 0, to: 0.7)
			.stroke(Color.green, lineWidth: 5)
			.frame(width: 100, height: 100)
	}
}

Как мы будем вращать круг? Мы используем модификаторы rotationEffect и animation. Суть в том, чтобы постоянно вращать круг на 360 градусов. Вот код:

struct ContentView: View {
	@State private var isLoading = false
	var body: some View {
		   Circle()
			.trim(from: 0, to: 0.7)
			.stroke(Color.green, lineWidth: 5)
			.frame(width: 100, height: 100)
			.rotationEffect(Angle(degrees: isLoading ? 360 : 0)) .animation(.default.repeatForever(autoreverses: false), value: isLoading)
			.onAppear() {
				isLoading = true
			}
   }
}

Модификатор rotationEffect принимает угол поворота (360 градусов). В коде выше у нас есть переменная состояния для управления статусом загрузки. Когда она установлена в значение true, угол поворота будет установлен в 360 градусов для вращения круга. В модификаторе анимации мы указываем использовать анимацию .default, и мы говорим SwiftUI повторять ту же анимацию снова и снова. Этот прием создает анимацию загрузки.

Примечание: если вы не видите анимацию в предварительном просмотре, запустите приложение в эмуляторе.

Если вы хотите изменить скорость анимации, вы можете использовать линейную анимацию и указать продолжительность следующим образом:

struct ContentView: View {
	@State private var isLoading = false
	var body: some View {
		   Circle()
			.trim(from: 0, to: 0.7)
			.stroke(Color.green, lineWidth: 5)
			.frame(width: 100, height: 100)
			.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
			.animation(.linear(duration: 5).repeatForever(autoreverses: false), value: isLoading)
			.onAppear() {
				isLoading = true
			}
   }
}

Чем больше значение duration, тем медленнее анимация (вращение). Модификатор onAppear может быть нов для вас. Если у вас есть некоторые знания UIKit, этот модификатор очень похож на viewDidAppear. Он автоматически вызывается, когда представление отображается на экране. 

В коде мы изменяем статус загрузки на true, чтобы начать анимацию при загрузке представления. C помощью этой техники, вы можете настроить дизайн и разработать различные версии индикатора загрузки. Например, вы можете наложить дугу на круг, чтобы создать красивый индикатор загрузки.

struct ContentView: View {
	@State private var isLoading = false
	var body: some View {
		ZStack {
			Circle()
				.stroke(Color(.systemGray5), lineWidth: 14)
				.frame(width: 100, height: 100)
			Circle()
				.trim(from: 0, to: 0.2)
				.stroke(Color.green, lineWidth: 7)
				.frame(width: 100, height: 100)
				.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
				.animation(.linear(duration: 1).repeatForever(autoreverses: false), value: isLoading)
				.onAppear() {
					isLoading = true
				}
		}
	}
}

Индикатор загрузки не обязательно должен быть круглым. Вы также можете использовать Rectangle или RoundedRectangle для создания индикатора. Вместо изменения угла поворота, вы можете изменить значение смещения, чтобы создать анимацию, как показано ниже.

Для создания анимации мы накладываем друг на друга два скругленных прямоугольника. Верхний прямоугольник гораздо короче, чем нижний. Когда загрузка начинается, мы обновляем его значение смещеЭтот код перемещает зеленый прямоугольник вдоль линии. Когда вы повторяете одну и ту же анимацию снова и снова, она становится анимацией загрузки.ния с -110 до 110.

struct ContentView: View {
	@State private var isLoading = false
	var body: some View {
		ZStack {
			Text("Loading")
				.font(.system(.body, design: .rounded))
				.bold()
				.offset(x: 0, y: -25)
			RoundedRectangle(cornerRadius: 3)
				.stroke(Color(.systemGray5), lineWidth: 3)
				.frame(width: 250, height: 3)
			RoundedRectangle(cornerRadius: 3)
				.stroke(Color.green, lineWidth: 3)
				.frame(width: 30, height: 3)
				.offset(x: isLoading ? 110 : -110, y: 0)
				.animation(.linear(duration: 1).repeatForever(autoreverses: false), value: isLoading)
		}
		.onAppear() {
			self.isLoading = true
		}
	}
}

Этот код перемещает зеленый прямоугольник вдоль линии. Когда вы повторяете одну и ту же анимацию снова и снова, она становится анимацией загрузки.

Создание индикатора прогресса

Индикатор загрузки предоставляет пользователю обратную связь о том, что приложение выполняет какую-то работу. Однако он не показывает фактический прогресс. Если вы хотите предоставить пользователям больше информации о ходе выполнения задачи, вам может потребоваться создать индикатор прогресса.

Создание индикатора прогресса очень похоже на создание индикатора загрузки. Вам нужна переменная состояния, чтобы отслеживать прогресс. Вот фрагмент кода для создания индикатора:

struct ContentView: View {
	@State private var progress: CGFloat = 0.0
	var body: some View {
		ZStack {
			Text("\(Int(progress * 100))%")
				.font(.system(.title, design: .rounded))
				.bold()
			Circle()
				.stroke(Color(.systemGray5), lineWidth: 10)
				.frame(width: 150, height: 150)
			Circle()
				.trim(from: 0, to: progress)
				.stroke(Color.green, lineWidth: 10)
				.frame(width: 150, height: 150)
				.rotationEffect(Angle(degrees: -90))
		}
		.onAppear() {
			Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
				progress += 0.05
				print(progress)
				if progress >= 1.0 {
					timer.invalidate()
				}
			}
		}
	}
}

Вместо логической переменной состояния мы используем число с плавающей точкой для хранения статуса. Для отображения прогресса мы устанавливаем модификатор trim с значением прогресса. В реальном приложении вы можете обновлять значение прогресса, чтобы показать фактический прогресс операции. Для этой демонстрации мы использовали таймер, который обновляет прогресс каждые полсекунды.

Задержка анимации

SwiftUI позволяет вам управлять длительностью анимации, но вы также можете задерживать анимацию с помощью функции delay следующим образом:

Animation.default.delay(1.0)

Это задержит начало анимации на 1 секунду. Функция delay применима к другим анимациям. Смешивая и сопоставляя значения длительности и задержки, вы можете достичь некоторых интересных анимаций, таких как индикатор загрузки ниже.

struct ContentView: View {
	@State private var isLoading = false
	var body: some View {
		ZStack {
			HStack {
				ForEach(0...4, id: \.self) { index in
					Circle()
						.frame(width: 10, height: 10)
						.foregroundColor(.green)
						.scaleEffect(self.isLoading ? 0 : 1) 
						.animation(.linear(duration: 0.6).repeatForever().delay(0.2 * Double(index)),
								   value: isLoading)
				}
			}
			.onAppear() {
				isLoading = true
			}
		}
	}
}

Этот индикатор состоит из пяти точек. Каждая тока анимируется для масштабирования, но с разными временными задержками. 

Сначала мы используем HStack для горизонтального расположения кругов. Поскольку все пять кругов (точки) имеют одинаковый размер и цвет, мы используем ForEach для создания кругов. Модификатор scaleEffect используется для масштабирования размера круга. По умолчанию он установлен в 1, что соответствует его первоначальному размеру. Когда загрузка начинается, значение обновляется до 0. Это уменьшит точку. Строка кода для рендеринга анимации выглядит немного сложной. Разберем ее по частям и рассмотрим пошагово:

.animation(.linear(duration: 0.6).repeatForever().delay(0.2 * Double(index)),
								   value: isLoading)

Первая часть создает линейную анимацию продолжительностью 0,6 секунды. Эта анимация должна выполняться постоянно, поэтому мы вызываем функцию repeatForever. Если запустить анимацию без вызова функции задержки, все точки будут масштабироваться вверх и вниз одновременно. Однако, это не то, чего мы хотим. 

Вместо масштабирования всех точек одновременно, каждая точка должна изменять свой размер независимо. Поэтому мы вызываем функцию задержки и используем разное значение задержки для каждой точки (на основе ее порядка в строке).

Преобразование прямоугольника в круг

Иногда вам, вероятно, нужно плавно преобразовывать одну форму (например, прямоугольник) в другую (например, круг). С помощью встроенной формы и анимации вы можете легко создать это преобразование

Суть преобразования прямоугольника в круг заключается в использовании формы RoundedRectangle и анимации изменения радиуса угла. Предполагая, что ширина и высота прямоугольника одинаковы, он превращается в круг, когда его радиус угла установлен в половину его ширины. Вот реализация преобразования кнопки:

struct ContentView: View {
	@State private var recordBegin = false
	@State private var recording = false

	var body: some View {
		ZStack {
			RoundedRectangle(cornerRadius: recordBegin ? 30 : 5)
				.frame(width: recordBegin ? 60 : 250, height: 60)
				.foregroundColor(recordBegin ? .red : .green)
				.overlay(
					Image(systemName: "mic.fill")
						.font(.system(.title))
						.foregroundColor(.white)
						.scaleEffect(recording ? 0.7 : 1)
				)
			RoundedRectangle(cornerRadius: recordBegin ? 35 : 10)
				.trim(from: 0, to: recordBegin ? 0.0001 : 1)
				.stroke(lineWidth: 5)
				.frame(width: recordBegin ? 70 : 260, height: 70)
				.foregroundColor(.green)
		}
		.onTapGesture {
			withAnimation(Animation.spring()) {
				recordBegin.toggle()
			}
			withAnimation(Animation.spring().repeatForever().delay(0.5)) { recording.toggle()
			}
		}
	}
}

У нас есть две переменные состояния: recordBegin и recording для управления двумя отдельными анимациями. Первая переменная управляет преобразованием кнопки. Как объяснялось ранее, мы используем радиус угла для преобразования. Ширина прямоугольника изначально установлена в 250 пунктов. 

Когда пользователь нажимает на прямоугольник для запуска преобразования, ширина фрейма изменяется на 60 пунктов. Вместе с этим изменением радиус угла изменяется на 30 пунктов, что составляет половину ширины. Таким образом, мы преобразовываем прямоугольник в круг. SwiftUI автоматически выполняет анимацию этого преобразования. Переменная состояния recording управляет масштабированием изображения микрофона. Мы изменяем коэффициент масштабирования с 1 до 0,7, когда он находится в состоянии записи.  (см. 23 строку)

Запуская ту же анимацию повторно, создается пульсирующая анимация. Обратите внимание, что код выше использует явный подход для анимации вью. Это необязательно - Вы также можете использовать неявный подход анимации для достижения того же результата.

Понимание переходов

То, о чем мы говорили до сих пор, это анимация представления, которое уже существует в иерархии представлений. Мы анимируем размер представления, его угол наклона или другие параметры. SwiftUI позволяет разработчикам делать больше, чем это. 

Вы можете определить, как представление будет добавляться или удаляться из иерархии представлений. В SwiftUI это называется переходом (transition). По умолчанию фреймворк использует переход fade in и fade out. Однако, помимо этого он предоставляет несколько готовых к использованию переходов, таких как slide, move, opacity и т.д. Конечно, вы можете разработать свой собственный или просто сочетать различные типы переходов вместе, чтобы создать желаемый кастомный переход.

Построение простого перехода

Давайте рассмотрим простой пример, чтобы лучше понять, что такое переход и как он работает с анимациями. Создайте новый проект с именем SwiftUITransition и обновите ContentView следующим образом:

struct ContentView: View {
	var body: some View {
		VStack {
			RoundedRectangle(cornerRadius: 10)
				.frame(width: 300, height: 300)
				.foregroundColor(.green)
				.overlay(
					Text("Покажи детальную информацию")
						.font(.system(.largeTitle, design: .rounded))
						.bold()
						.foregroundColor(.white)
				)
			RoundedRectangle(cornerRadius: 10)
				.frame(width: 300, height: 300)
				.foregroundColor(.purple)
				.overlay(
					Text("Вот твоя детальная информация")
						.font(.system(.largeTitle, design: .rounded))
						.bold()
						.foregroundColor(.white))
		}
	}
}

В коде выше мы располагаем два квадрата вертикально с помощью VStack. Сначала пурпурный прямоугольник должен быть скрыт. Он отображается только тогда, когда пользователь нажимает на зеленый прямоугольник (т.е. Показать детали). Чтобы показать пурпурный квадрат, нам нужно сделать зеленый квадрат нажимаемым.

Для этого нам нужно объявить переменную состояния, чтобы определить, показан ли фиолетовый квадрат или нет. Вставьте эту строку кода в ContentView:

@State private var show = false

Затем, чтобы скрыть фиолетовый квадрат, мы заворачиваем фиолетовый квадрат в такое предложение if:

			if show {
				RoundedRectangle(cornerRadius: 10)
					.frame(width: 300, height: 300)
					.foregroundColor(.purple)
					.overlay(
						Text("Вот твоя детальная информация")
							.font(.system(.largeTitle, design: .rounded))
							.bold()
							.foregroundColor(.white))
			}

Для VStack мы добавим функцию onTapGesture для обнаружения нажатия и создания анимации для изменения состояния. Обратите внимание, что переход должен быть связан с анимацией, иначе он не будет работать самостоятельно.

struct ContentView: View {

	@State private var show = false

	var body: some View {
		VStack {
			RoundedRectangle(cornerRadius: 10)
				.frame(width: 300, height: 300)
				.foregroundColor(.green)
				.overlay(
					Text("Покажи детальную информацию")
						.font(.system(.largeTitle, design: .rounded))
						.bold()
						.foregroundColor(.white)
				)
			if show {
				RoundedRectangle(cornerRadius: 10)
					.frame(width: 300, height: 300)
					.foregroundColor(.purple)
					.overlay(
						Text("Вот твоя детальная информация")
							.font(.system(.largeTitle, design: .rounded))
							.bold()
							.foregroundColor(.white))
			}
		}
		.onTapGesture {
			withAnimation(.spring()) {
				show.toggle()
			}
		}
	}
}

Когда пользователь нажимает на VStack, мы переключаем переменную show, чтобы отобразить пурпурный квадрат. Если вы запустите приложение в эмуляторе или на предварительном просмотре, вы увидите только зеленый квадрат. Нажатие на него отобразит пурпурный прямоугольник с плавным переходом fade in/out.

Как упоминалось ранее, если вы не укажете переход, который хотите использовать, SwiftUI будет выполнять переход fade in и fade out. Чтобы использовать альтернативный переход, присоедините модификатор перехода к пурпурному квадрату следующим образом:

if show {
				RoundedRectangle(cornerRadius: 10)
					.frame(width: 300, height: 300)
					.foregroundColor(.purple)
					.overlay(
						Text("Вот твоя детальная информация")
							.font(.system(.largeTitle, design: .rounded))
							.bold()
							.foregroundColor(.white))
					.transition(.scale(scale: 0, anchor: .bottom))
			}

Модификатор перехода принимает параметр типа AnyTransition. Здесь мы используем переход с масштабированием и с якорем установленным в нижней части. Это все, что вам нужно сделать, чтобы изменить переход. 

Запустите приложение в эмуляторе. Вы должны увидеть всплывающую анимацию, когда приложение показывает пурпурный квадрат. Тестировать анимации лучше с помощью эмулятора вместо запуска приложения в предварительном просмотре, потому что холст предварительного просмотра может некорректно отображать переход.

В дополнение к .scale, фреймворк SwiftUI поставляется с несколькими встроенными переходами, включая .opaque, .offset, .move и .slide. Замените переход .scale на переход .offset следующим образом:

.transition(.offset(x: -600, y: 0))

На этот раз фиолетовый квадрат скользит слева, когда он должен отобразиться в VStack.

Комбинирование Переходов

Вы можете объединить два или более перехода вместе, вызвав метод combined(with:), чтобы создать еще более плавный переход. Например, для объединения анимации смещения и масштабирования, вы можете написать код следующим образом:

.transition(.offset(x: -600, y: 0).combined(with: .scale))

Вот еще один пример, который сочетает в себе три перехода:

.transition(.offset(x: -600, y: 0)
						.combined(with: .scale)
						.combined(with: .opacity))

Иногда вам может быть нужна своя многоразовая анимация. Вы можете определить расширение для AnyTransition следующим образом:

extension AnyTransition {
	static var offsetScaleOpacity: AnyTransition {
		AnyTransition.offset(x: -600, y: 0).combined(with: .scale).combined(with: .opacity)
	}
}

Соответственно теперь вы сможете использовать эту настройку как отдельную анимацию

.transition(.offsetScaleOpacity)

Весь код целиком будет выглядеть следующим образом

extension AnyTransition {
	static var offsetScaleOpacity: AnyTransition {
		AnyTransition.offset(x: -600, y: 0).combined(with: .scale).combined(with: .opacity)
	}
}

struct ContentView: View {

	@State private var show = false

	var body: some View {
		VStack {
			RoundedRectangle(cornerRadius: 10)
				.frame(width: 300, height: 300)
				.foregroundColor(.green)
				.overlay(
					Text("Покажи детальную информацию")
						.font(.system(.largeTitle, design: .rounded))
						.bold()
						.foregroundColor(.white)
				)
			if show {
				RoundedRectangle(cornerRadius: 10)
					.frame(width: 300, height: 300)
					.foregroundColor(.purple)
					.overlay(
						Text("Вот твоя детальная информация")
							.font(.system(.largeTitle, design: .rounded))
							.bold()
							.foregroundColor(.white))
					.transition(.offsetScaleOpacity)
			}
		}
		.onTapGesture {
			withAnimation(.spring) {
				show.toggle()
			}
		}
	}
}

Асимметричные переходы

Переходы, которые мы только что обсудили, все симметричны, что означает, что добавление и удаление вью используют один и тот же переход. Например, если вы применяете переход по масштабу к представлению, SwiftUI масштабирует представление при его вставке в иерархию представлений. Когда оно удаляется, фреймворк масштабирует его обратно до нулевого размера. А что, если вы хотите использовать переход по масштабу при вставке представления и переход смещения при его удалении? Это называется асимметричными переходами в SwiftUI. Очень просто использовать этот тип перехода. Вам просто нужно вызвать метод .assymetric и указать переходы вставки и удаления. Вот пример кода:

.transition(.asymmetric(insertion: .scale(scale: 0, anchor: .bottom), removal: .offset(x: -600, y: 0)))

Разумеется такой вид перехода также можно добавить в расширение чтобы потом использовать повторно

extension AnyTransition {
	static var scaleAndOffset: AnyTransition {
		AnyTransition.asymmetric(
			insertion: .scale(scale: 0, anchor: .bottom),
			removal: .offset(x: -600, y: 00)
		)
	}
}

И затем использовать его в коде следующим образом

.transition(.scaleAndOffset)

Итоги

Анимация играет особую роль в дизайне мобильного пользовательского интерфейса.

Продуманная анимация улучшает пользовательский опыт и придает смысл взаимодействию с UI. Плавный и легкий переход между двумя представлениями принесет удовольствие и впечатлит ваших пользователей. С более чем 2 миллионами приложений в App Store нелегко сделать так, чтобы ваше приложение выделялось. Однако хорошо спроектированный UI с анимацией точно сможет выгодно выделить его!

Даже для опытных разработчиков нелегкая задача написать плавные, чистые и понятные анимации. К счастью, фреймворк SwiftUI упростил разработку анимации и переходов. Вы говорите фреймворку, как должно выглядеть представление в начале и в конце, а SwiftUI вычисляет все что для этого нужно, создавая плавную и приятную анимацию. В этой части мы познакомились с основами и при этом  вы уже создали несколько замечательных анимаций и переходов. Самое главное, это потребовало всего нескольких строк кода.

Как и прежде подписывайтесь на мой телеграм канал - https://t.me/swiftexplorer

В ближайшее время выйдут следующие части уроков по SwiftUI. 

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

Комментарии (0)