Сделаем на 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)


  1. petertretyakov
    28.06.2019 09:54
    -1

    Шустро Вы у Менга туториал перевели :) Хоть бы ссылку на первоисточник оставили, там же не только про карточки.


    1. IvanVorobei Автор
      28.06.2019 09:58

      Это не перевод его туториала. Взял диз и анимации — повторил — написал туториал.


      Не указываю источник потому что ссылка на ресурсы, где можно что-то купить, переводит статью в «Я пиарюсь» и исключает из всех хабов.


  1. itmuslim
    28.06.2019 15:36
    +1

    Спасибо. Красиво получилось.


    1. IvanVorobei Автор
      28.06.2019 15:37

      Дизайн взял у этого парня, он умеет делать эффектные вещи


      1. itmuslim
        28.06.2019 17:59

        Действительно умеет. Благодарю за наводку!


  1. matrixs
    30.06.2019 19:48

    Для удобства вынесем код в отдельный класс MainCard:

    struct MainCard: View {

    Может я скажу глупость, но это же структура


    1. IvanVorobei Автор
      30.06.2019 20:29

      Все никак не могу забыть UIKit)
      Поправил, спасибо)