Сделаем на 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.
Если вам удобнее смотреть видео, гляньте туториал. Кстати, в видео я использую реалтайм превью.
Комментарии (7)

itmuslim
28.06.2019 15:36+1Спасибо. Красиво получилось.

matrixs
30.06.2019 19:48Для удобства вынесем код в отдельный класс MainCard:
struct MainCard: View {
Может я скажу глупость, но это же структура
petertretyakov
Шустро Вы у Менга туториал перевели :) Хоть бы ссылку на первоисточник оставили, там же не только про карточки.
IvanVorobei Автор
Это не перевод его туториала. Взял диз и анимации — повторил — написал туториал.
Не указываю источник потому что ссылка на ресурсы, где можно что-то купить, переводит статью в «Я пиарюсь» и исключает из всех хабов.