Ваше воображение - единственный предел

Примечание. Все используемые здесь банковские карты, конечно же, поддельные

Вступление

Прошла пара месяцев с момента выпуска iOS 13, и хотя еще слишком рано для полной поддержки в наших приложениях, стоит взглянуть на две основные платформы, которые были представлены в этой новой версии: SwiftUI и Combine. Без сомнения, это будущее разработки под iOS.

Последние несколько месяцев я экспериментировал со SwiftUI. Даже если мне все еще не хватает некоторых вещей, я был приятно удивлен тем, насколько легко настроить пользовательский интерфейс, а также анимировать его. То, что потребует большого количества кода в UIkit, может быть выполнено парой строк в SwiftUI. И что самое приятное, все проходит гладко и быстро! Он обходит Core Animation и переходит прямо к Metal.

Забудь об UIkit и двигайся дальше, потому что ты остаешься позади, приятель!

Банковские карты: краткий обзор

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

Если вы посмотрите на стартовый проект в моем репозитории, вы, возможно, захотите сначала проверить пару файлов, поскольку именно над ними мы будем работать: Wallet.swift и WalletView.swift. Первый моделирует кошелек и в основном содержит кучу карточек и предоставляет вспомогательные методы. Второй - складывает карты одна над другой и применяет некоторые изменения пользовательского интерфейса, чтобы красиво отображать карты в вашем кошельке:

Результат такой:

Анимируйте переход вашего кошелька

Как я уже упоминал ранее, SwiftUI упрощает создание необычных дизайнов. Как тогда это работает? SwiftUI за кулисами использует Combine и предоставляет три оболочки свойств (новые в Swift 5.1), которые помогут вам в этом процессе. Они в основном сообщают представлению, что что-то изменилось, что вызовет обновление представления. Эти обертки свойств:

  • @State: он представляет свойство представления, которое содержит некоторое состояние, на которое представление полагается для рендеринга.
  • @ObservedObject: это объект, свойства которого наблюдаются View.
  • @EnvironmentObject: аналогично @ObservedObject, но глобально доступен для View и его подпредставлений.

Давай приступим к работе! Сейчас бумажник отображается на экране без анимации - ничего особенного не происходит. Почему бы нам не перейти на презентацию кошелька? Объявите новое свойство isPresented и сначала установите для него значение false:

@State var isPresented = false

Измените код внутри ZStack, чтобы отображать карты, если isPresented истинно. Затем используйте onAppear для переключения флага.

Теперь добавьте неявную анимацию для анимации перехода CardView:

  1. Вызов transition, чтобы всякий раз, когда карта добавляется к WalletView, она отображалась движущейся вверх с постепенным исчезновением.
  2. Используйте animation, чтобы неявно указать, как должна анимироваться каждая карточка и ее подпредставления.

Другой способ сделать это - использовать явную анимацию:

withAnimation {
     self.isPresented.toggle()
}

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

Когда вы закончите, у вас должно получиться что-то вроде этого:

Перетащите карты, чтобы отсортировать кошелек

Теперь наш кошелек выглядит лучше, но недостаточно. Вот что мы собираемся делать сейчас. Мы собираемся сделать наш CardView перетаскиваемым, чтобы мы могли сортировать карты в кошельке, перетаскивая карту вперед или назад вверх или вниз. Для этого нам нужно добавить DragGesture к нашему первому CardView экземпляру:

DragGesture поставляется с двумя блоками. onChanged вызывается каждый раз, когда DragGesture обновляет свой перевод, тогда как onEnded вызывается всякий раз, когда жест завершается. Мы отключим взаимодействие с пользователем на карточках, которые находятся сзади, чтобы перетаскивать только первую.

В файле уже определены некоторые свойства и вспомогательные методы, которые мы будем использовать в этом разделе. Взгляните на offset(for:):

private func offset(for card: Card) -> CGFloat {
    guard !wallet.isFirst(card: card) else { return draggingOffset }
    let cardIndex = CGFloat(wallet.index(of: card))
    return cardIndex * Self.cardOffset
}

Это функция, которая была вызвана для компенсации карт в зависимости от их положения в кошельке. dragginOffset объявляется в верхней части файла и инициализируется значением 0. Мы будем использовать onChanged, чтобы обновить его значение:

self.draggingOffset = value.translation.height

Ой, подожди! Мы получаем сообщение об ошибке:

WalletView и любой другой View в SwiftUI - это struct, что означает, что они неизменяемы. К счастью для нас, SwiftUI позволяет нам изменять их свойства, если мы заключаем их в @State.

@State var draggingOffset: CGFloat = 0

Это вызовет обновление анимированного представления, которое мы неявно откладывали. Перейдите к ForEach внутри вас WalletView и добавьте:

ForEach(self.wallet.cards) {
    // code here
}.onAppear {
    self.shouldDelay = false
}

Это заставит вспомогательный метод transitionDelay(card:) возвращать 0 после появления ForEach. Не забудьте заключить shouldDelay в оболочку, чтобы избавиться от ошибки компилятора:

@State var shouldDelay = true

Наконец, используйте onEnded, чтобы вернуть draggingOffset в 0:

onEnded ({ _ in
    self.draggingOffset = 0
})

Теперь, чтобы отсортировать карты в кошельке, добавьте в onEnded следующий код:

let newCards = [card] + Array(self.wallet.cards.dropLast())
self.wallet.cards = newCards

Последний штрих: коснитесь карты, чтобы вынести ее на передний план

Чтобы закончить этот урок, давайте добавим TapGesture к нашим картам, чтобы вынести выбранную карту в начало стопки. Во-первых, избавьтесь от этой строки

.disabled(!self.wallet.isFirst(card: card))

и измените DragGesture onChanged и onEnded так, чтобы перетаскивалась только передняя карта:

onChanged ({ _ in
    if self.wallet.isFirst(card: card) {
        // code here
    }
})
.onEnded ({ _ in
    if self.wallet.isFirst(card: card) {
        // code here
    }
})

Теперь используйте onTapGesture, чтобы снова отсортировать карточки:

.gesture(
    DragGesture()
        // ...
).onTapGesture {
    let newCards = self.wallet.cards.filter { $0 != card } + [card]
    self.wallet.cards = newCards
}

Строй и беги. Не совсем получили ожидаемый результат? Попробуйте обернуть wallet @State. Еще ничего? Вот что происходит. WalletView не знает, что его нужно обновить. Нам нужно добавить другую оболочку к wallet, @ObservedObject:

@ObservedObject var wallet: Wallet = Wallet(cards: cards)

На этом этапе компилятор должен пожаловаться: Ссылка на инициализатор «init (wrappedValue :)» в «ObservedObject» требует, чтобы «Wallet» соответствовал «ObservableObject».

Перейдите к Wallet.swift и выполните ObservableObject:

class Wallet: ObservableObject {
    // ...
}

Однако что именно следует учитывать в SwiftUI? Нам нужно явно указать, какие свойства следует соблюдать, или, другими словами, какие свойства будут опубликованы:

@Published var cards: [Card]

Строй и беги. На этот раз у нас должен быть ожидаемый результат.

Куда пойти отсюда?

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

Ознакомьтесь с улучшениями, которые я внес в финальную версию этого руководства.

Заключение

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

  • Он использует металл и поэтому работает очень быстро и плавно.
  • Легко и просто использовать.
  • Понятно в отличие от базового кода XIB или раскадровки. С конфликтами легче справляться.
  • Более разборчивый, чем ограничения кодирования.
  • Меньше строк кодов

С другой стороны, есть некоторые минусы:

  • Он довольно новый, поэтому постоянно меняется.
  • Некоторым представлениям, которые широко используются в UIkit, нет эквивалента в SwiftUI. Например, UICollectionView или TextView.
  • Интеграция в ваше приложение, поскольку SwiftUI поддерживается только iOS 13+.

Ресурсы