Сделаем на 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 разработке.
