Сделаем на SwiftUI
анимированные карточки с поддержкой жестов:
Хотел добавить подробное превью, но размер гифки становится не православный. Большое превью можно глянуть по ссылке или в видео-туториале.
Потребуется
SwiftUI
сейчас в beta, и устанавливается вместе с новым Xcode, который тоже в beta. Хорошая новость — новый Xcode можно поставить рядом со старым, и практически не почувствовать боли.
Скачать его можно по ссылке в разделе Applications
.
Вы могли встречать риалтайм-превью во время работы со SwiftUI
. Чтобы активировать его, а так же некоторые контекстные меню, нужно установить бету macOS Catalina
. Тут без боли не обойдется. Я бету не ставил, поэтому буду по старинке запускать симулятор.
Новый проект
Создайте Single View Application
с дополнительной галочкой — SwiftUI
:
Перейдите в файл ContentView.swift. Наследник PreviewProvider
отвечает за превью. Так как его использовать не будем, оставим минимально необходимый код:
import SwiftUI
struct ContentView: View {
var body: some View {
}
}
Надеюсь общее понимание о SwiftUI
уже есть, на тривиальных моментах останавливаться не будем.
Карточки
Карточки расположены друг за другом, поэтому будем использовать ZStack
. Напомню, есть ещё два варианта группирования элементов: HStack
— горизонтально и VStack
— вертикально. Для наглядности:
Добавим первую карточку:
struct ContentView: View {
var body: some View {
return ZStack {
Rectangle()
.fill(Color.black)
.frame(height: 230)
.cornerRadius(10)
.padding(16)
}
}
}
Здесь добавили прямоугольник, покрасили в черный цвет, высоту 230pt
, закруглили края на 10pt
и установили отступы со всех сторон 16pt
.
Текст в карточку добавляется в блок ZStack
после прямоугольника:
Text("Main Card")
.color(.white)
.font(.title)
.bold()
Запустите проект, чтобы увидеть промежуточный результат:
Но их же три!
Для удобства вынесем код MainCard
:
struct MainCard: View {
var title: String
var body: some View {
ZStack {
Rectangle()
.fill(Color.black)
.frame(height: 230)
.cornerRadius(10)
.padding(16)
Text(title)
.color(.white)
.font(.largeTitle)
.bold()
}
}
}
Проперти title
появится в инициализаторе. Этот текст будет в карточке. Добавим карточку в ContentView
, заодно увидим новый параметр в инициализаторе:
struct ContentView: View {
var body: some View {
return MainCard(title: "Main Card")
}
}
Уже умеем выносить код, поэтому сразу определим класс для фоновых карточек:
struct Card: View {
var title: String
var body: some View {
ZStack {
Rectangle()
.fill(Color(red: 68 / 255, green: 41 / 255, blue: 182 / 255))
.frame(height: 230)
.cornerRadius(10)
.padding(16)
Text(title)
.color(.white)
.font(.title)
.bold()
}
}
}
Установили другой цвет и стиль для текста. В остальном код повторяет главную черную MainCard
. Добавим две фоновые карточки в ContentView
. Карточки расположены друг за другом, поэтому поместим их в ZStack
. Код ContentView
:
struct ContentView: View {
var body: some View {
return ZStack {
Card(title: "Third card")
Card(title: "Second Card")
MainCard(title: "Main Card")
}
}
}
Фоновые карточки расположены под черной и пока их не видно. Добавим смещение вверх и отступы от краев:
Card(title: "Third card")
.blendMode(.hardLight)
.padding(64)
.padding(.bottom, 64)
Card(title: "Second Card")
.blendMode(.hardLight)
.padding(32)
.padding(.bottom, 32)
MainCard(title: "Main Card")
Теперь результат напоминает анонс в начале туториала:
Перейдем к жестам, а вместе с этим к анимациям.
Жесты
То, как реализованы жесты, вынудит минусануть мне карму и оставить токсичный отзыв.
Перед тем, как увидите код, обращаю внимание — он приведен на developer.apple.com в качестве примера. Первое впечатление обманчиво, на практике мне понравилось.
Объявим enum в ContentView
:
enum DragState {
case inactive
case dragging(translation: CGSize)
var translation: CGSize {
switch self {
case .inactive:
return .zero
case .dragging(let translation):
return translation
}
}
var isActive: Bool {
switch self {
case .inactive:
return false
case .dragging:
return true
}
}
}
DragState
сделает работу с жестом комфортней. Добавим проперти dragState
в ContentView
:
@GestureState var dragState = DragState.inactive
Здесь будет магия.
Везде, где будет использоваться dragState
, новые значения будут применятся автоматически. Объявим жест:
let dragGester = DragGesture()
.updating($dragState) { (value, state, transaction) in
state = .dragging(translation: value.translation)
}
Добавим жест главной карточке и установим offset
:
MainCard(title: "Main Card")
.offset(
x: dragState.translation.width,
y: dragState.translation.height
)
.gesture(dragGester)
Смещение будет равно значению параметра в момент обращения к нему? Нет, будет равно всегда. Это и есть магия.
Карточка будет следовать за пальцем мышкой, но без анимаций:
Фоновые карточки тоже должны менять свою позицию (по крайней мере мы так захотели). Добавим для них код, привязанный к состоянию жеста. rotation3DEffect
повернет карточку, пока жест не станет активным:
Card(title: "Third card")
.rotation3DEffect(Angle(degrees: dragState.isActive ? 0 : 60), axis: (x: 10.0, y: 10.0, z: 10.0))
.blendMode(.hardLight)
.padding(dragState.isActive ? 32 : 64)
.padding(.bottom, dragState.isActive ? 32 : 64)
Card(title: "Second Card")
.rotation3DEffect(Angle(degrees: dragState.isActive ? 0 : 30), axis: (x: 10.0, y: 10.0, z: 10.0))
.blendMode(.hardLight)
.padding(dragState.isActive ? 16 : 32)
.padding(.bottom, dragState.isActive ? 0 : 32)
Как еще можно использовать 3D можно глянуть тут. Также я добавил blendMode
. Режимы аналогичны инструментам в Photoshop и Sketch.
Пока изменения применяются не анимировано, давайте исправим.
Анимация
Вы удивитесь насколько это просто. Достаточно добавить строчку:
.animation(.spring())
Добавьте для каждой карточки. Теперь любые изменения будут применятся анимировано, в нашем случае это отступы-размеры, 3D поворот и offset
. Если нужна анимацию с кривыми, используйте режим basic
.
Украшательства
Добавим тень и поворот главной вью, привязанный к смещению:
.rotationEffect(Angle(degrees: Double(dragState.translation.width / 10)))
.shadow(radius: dragState.isActive ? 8 : 0)
Если запустите проект, то увидите референс из начала туториала. Поворот на превью не видно, потяните карточку в сторону чтобы увидеть эффект.
Для ищущих
→ Код проекта можно найти в репозитории
Достаточно скопировать файл в проект, никаких дополнительный настроек не требуется. Не пугайтесь что кода так мало — это одна из плюшек SwiftUI
.
Если вам удобнее смотреть видео, гляньте туториал. Кстати, в видео я использую реалтайм превью.
Загляните в мой телеграм-канал по iOS разработке.