SA0710 — Жесты (Gestures)
Содержание страницы
При разработке привлекательного и веселого пользовательского интерфейса в современном мобильном приложении часто бывает полезно добавить дополнительную динамику взаимодействию пользователей. Смягчение прикосновения или увеличение текучести между визуальными обновлениями может сделать разницу между полезным приложением и важным приложением.
В этой главе вы расскажете о том, как можно добавлять, комбинировать и настраивать пользовательские взаимодействия, такие как жесты, чтобы создать уникальный пользовательский интерфейс, интуитивно понятный и новый.
Вы вернетесь к приложению Kuchi flashcard, описанному в предыдущих главах; вы добавите элемент панели вкладок и новый вид для изучения новых слов. До сих пор приложение позволяет вам практиковать слова, которые вы можете знать или не знать, но в нем нет функции изучения вводных слов.
Для того чтобы проект был готов к реализации, предстоит проделать немалую работу. Только для этой главы вы найдете два стартовых проекта в папке starter, содержащихся в этих папках:
- starter-глава
- стартер-жесты
Если вы хотите выполнить всю подготовительную работу, либо повторно используйте проект, который вы завершили в предыдущей главе, либо используйте проект, содержащийся в starter-chapter, и продолжайте читать.
Если вы хотите пропустить подготовительную работу и сразу перейти к жестам, то пропустите следующий раздел Добавления функции learn (но все равно рекомендуется хотя бы бегло взглянуть) и начните читать Свой первый жест.
Если вы решили принять синюю таблетку, начните с открытия стартового проекта из папки starter/starter-chapter — или вашего собственного проекта, взятого из предыдущего проекта, если хотите.
Добавление функции обучения
В предыдущей главе вы добавили в приложение панель вкладок, состоящую только из двух вкладок: Вызов и Настройки. Теперь вы собираетесь добавить 3-ю вкладку, занимающую первую позицию в списке вкладок, которая будет заботиться о разделе Learn.
Сначала вам нужно создать пустое представление в качестве представления верхнего уровня для функции learn, которое будет состоять из нескольких файлов. Вы поместите их в новую группу под названием Learn. Он будет находиться на том же уровне, что и существующая папка практики.
Итак, в Навигаторе проектов щелкните правой кнопкой мыши общую группу, выберите Новую группуи назовите ее Learn.
Представление, которое вы будете строить, будет использоваться для изучения новых слов; поэтому его можно интуитивно вызвать LearnView
. Итак, продолжайте и создайте новый файл представления SwiftUI с именем LearnView.swift внутри группы Learn.
После того как вы создали новое представление, вы можете оставить его как есть и позаботиться о добавлении способа доступа к этому новому представлению, которое, как уже упоминалось, будет происходить в виде вкладки.
Откройте HomeView и перед PracticeView
вкладкой добавьте эту новую вкладку:
LearnView() .tabItem({ VStack { Image(systemName: "bookmark") Text("Learn") } }) .tag(0)
Если вы возобновите предварительный просмотр, вот что вы увидите:

Вновь созданная вкладка learn
Создание флэш-карты
С появлением новой вкладки Learn первым компонентом функции Learn, над которой вы будете работать, станет флэш-карта. Это должен быть простой компонент с оригинальным словом и переводом для запоминания.
Когда речь идет о карте, полезно распознать два различных понимания внутри приложения: визуальную карту (компонент пользовательского интерфейса) и данные карты (состояние).
И то, и другое является неотъемлемой частью функции карты, а сама карта представляет собой композицию обоих элементов. Однако визуальная карта не может существовать без состояния; для начала вам нужна структура данных, которая может представлять это состояние.
Используя шаблон файла Swift, создайте новый файл в папке Learn с именем FlashCard.swift. Пока это будет пустая структура — добавьте ее:
struct FlashCard { }
Внутри структуры вам понадобятся данные, которые пользователь пытается изучить. В данном случае это слово. Добавьте свойство типа Challenge
с именем card
в свою структуру:
var card: Challenge
Это базовая структура данных для вашей флэш-карты, но чтобы сделать ее полезной для ваших представлений SwiftUI, вам понадобится еще несколько свойств.
Во-первых, an id
может быть полезен для перебора нескольких карточек в представлении. Это лучше всего достигается путем приведения структуры в соответствие с Identifiable
протоколом, так как ForEach
блок SwiftUI будет искать anid
, если не указан явный идентификатор.
Поскольку в приложении нет id
генераторов, вы можете просто полагаться на Foundation``UUID
конструктор, чтобы предоставить уникальный идентификатор каждый разFlashCard
, когда создается a. Добавьте следующее свойство вFlashCard
:
let id = UUID()
Как видите, явного использования Identifiable
протокола пока нет. Об этом мы расскажем в ближайшее время. Последний шаг, необходимый в вашей базовой FlashCard
структуре состояния, — это добавить флаг called isActive
. Добавьте следующее свойство:
var isActive = true
Это простое свойство для фильтрации карточек, которые должны быть частью учебного сеанса.
Пользователь может не захотеть каждый раз перебирать всю колоду карт, которые он уже знает, поэтому это позволяет выборочно фильтровать карты либо через кураторство пользователя, либо через внутреннюю логику. Чтобы обеспечить соответствие Identifiable
протоколу, добавьте его в декларацию структуры:
struct FlashCard: Identifiable { ... }
Вам не нужно делать ничего лишнего, чтобы сделать FlashCard
его идентифицируемым, но вы захотите убедиться, что это так Equatable
. Это позволит вам быстро и легко проводить сравнения в коде, чтобы убедиться, что одна и та же карта не дублируется или что одна карта соответствует другой, когда это уместно.
Добавьте это расширение послеFlashCard
:
extension FlashCard: Equatable { static func == (lhs: FlashCard, rhs: FlashCard) -> Bool { return lhs.card.question == rhs.card.question && lhs.card.answer == rhs.card.answer } }
С помощью этого свойства вы сможете использовать ==
оператор для сравнения двух флэш-карт.
Вот и все; вот и ваш FlashCard
объект состояния определен и готов к использованию! Однако пользователь не будет изучать одну карту за раз, поэтому вам нужно будет опираться на этот объект с помощью концепции колоды. В приложении есть колода для функции практики в виде простого массива карт, но функция обучения имеет разные потребности, поэтому на этот раз вы будете более четко понимать, как работает колода.
Создание флэш-колоды
Хотя колода не является новой концепцией, функция Learn будет более явной, чем практика с колодой карт, создавая совершенно новую структуру состояний для использования в пользовательском интерфейсе. Поскольку вам нужны дополнительные свойства и возможности, требуется новый объект SwiftUI state. Точно так же новый объект колоды также будет адаптирован к состоянию SwiftUI.
Начните с создания нового файла Swift с именем FlashDeck.swift внутри группы Learn, используя шаблон файла Swift. FlashDeck
нужно только одно свойство: массив FlashCard
объектов — Добавьте следующий класс:
class FlashDeck { var cards: [FlashCard] }
То, что делает FlashDeck
мощный объект SwiftUI state object, происходит из двух модификаций. Первый будет из конструктора. Добавьте следующее:
init(from words: [Challenge]) { cards = words.map { FlashCard(card: $0) } }
Этот конструктор просто map
s слова (Challenge
ы), переданные в FlashCard
s.
Второе включение питания для FlashDeck
модели происходит от Combine
. Чтобы пользовательский интерфейс реагировал на изменения в колоде, cards
свойство будет иметь префикс @Published
атрибута, позволяющий подписчикам модели получать уведомления об обновлениях.
Изменить cards
свойство с:
var cards: [FlashCard]
В:
@Published var cards: [FlashCard]
И, наконец, вам нужно расширить класс до an ObservableObject
(в соответствии с главой 9: «Состояние и поток данных — часть II»).:
class FlashDeck: ObservableObject { ... }
Теперь у вас есть свойFlashCard
, FlashDeck
построенный и готовый к работе.
Конечное состояние
Ваша конечная государственная работа для функции Learn будет вашим магазином верхнего уровня, который будет содержать вашу колоду (и карты) и обеспечивать пользовательский контроль для управления вашей колодой и получения обновлений в вашем пользовательском интерфейсе. В соответствии со стандартами именования будет называться модель состояния верхнего уровня LearningStore
.
Создайте новый файл с именем LearningStore.swift в группе Learn, используя шаблон файла Swift.
Затем заполните файл следующими данными::
`class LearningStore {
// 1
@Published var deck: FlashDeck
// 2
@Published var card: FlashCard?
// 3
@Published var score = 0
// 4
init(deck: [Challenge]) {
self.deck = FlashDeck(from: deck)
self.card = getNextCard()
}
// 5
func getNextCard() -> FlashCard? {
guard let card = deck.cards.last else {
return nil
}
self.card = card
deck.cards.removeLast()
return self.card
}
}`
Рассмотрим это шаг за шагом:
- Как и в
FlashDeck
случае с , вы будете использоватьCombine``@Published
атрибуты для своих свойств. Магазин будет поддерживать полную колоду (deck
), - … текущая карта (
card
), - … и текущий счет (
score
). - Вы добавляете инициализатор, который настраивает колоду.
- Вы также добавляете удобный метод, который позволит получить следующую карту в колоде. Он делает это, удаляя последнюю карту из колоды и возвращая ее.
Последним шагом в создании этого магазина является приведение его в соответствие сObservableObject
:
class LearningStore: ObservableObject { ... }
Фух — это большая настройка без какого-либо кода пользовательского интерфейса, верно? Но теперь вы создали хорошую основу для создания представления для функции Learn.
И, наконец,… создание пользовательского интерфейса
Пользовательский интерфейс для функции Learn будет сформирован вокруг 3-уровневого представления. Первый-это ваш текущий пустой LearnView
. Второй, сидящий сверхуLearnView
, — это вид колоды, и, наконец, сидящий на палубе-это текущая флэш-карта.
Вы начнете с добавления недостающих представлений: DeckView
и CardView
.
Во-первых, все еще находясь в группе Learn, создайте файл представления SwiftUI с именем CardView.swift, используя шаблон представления SwiftUI, и замените его содержимое body
на:
ZStack { Rectangle() .fill(Color.red) .frame(width: 320, height: 210) .cornerRadius(12) VStack { Spacer() Text("Apple") .font(.largeTitle) .foregroundColor(.white) Text("Omena") .font(.caption) .foregroundColor(.white) Spacer() } } .shadow(radius: 8) .frame(width: 320, height: 210) .animation(.spring(), value: 0)
Это создает простой вид красной карточки с закругленными углами и парой текстовых меток по центру карточки. Вы подробно остановитесь на этом представлении позже в учебнике.
Если вы предварительно просмотрите это на холсте, то увидите следующее:

Колода карт
Далее-вид на палубу. Создайте файл SwiftUI с именем (как вы уже догадались) DeckView.swift и замените его содержимое body
на:
ZStack { CardView() CardView() }
Это простое представление, содержащее две карты, но вскоре вы дополните его, используя объекты состояний, созданные ранее, для поддержки загрузки динамически генерируемых карт в поток обучения.
Поскольку карты сложены друг на друга, предварительный просмотр вида колоды на холсте даст вам тот же результат, что и раньше.
Далее нужно добавить DeckView
в LearnView
.
Вернитесь в LearnView и замените содержимое body
следующим:
VStack { Spacer() Text("Swipe left if you remembered" + "\nSwipe right if you didn’t") .font(.headline) DeckView() Spacer() Text("Remembered 0/0") }
Это довольно просто: у вас есть Text
этикетка с инструкциями, оценка внизу и DeckView
в центре экрана.

Представление learn
Добавление LearningStore
к представлениям
Оставаясь внутри LearnView
, вы можете добавить в представление ранее созданное хранилище в качестве свойства:
@StateObject var learningStore = LearningStore(deck: ChallengesViewModel.challenges)
Как LearningStore
и a StateObject
, он может использоваться внутри the LearnView
для обеспечения перестройки представления при изменении любого из опубликованных свойств. С помощью этой настройки вы даже можете обновить счет Text
в нижней части представления.
Заменить:
Text("Remembered 0/0")
С:
`Text(«Remembered (learningStore.score)»
- «/(learningStore.deck.cards.count)»)`
Пока это хорошо. Вы вернетесь к LearnView
этому позже, но сейчас DeckView
необходимо иметь возможность получать некоторые данные из LearningStore
карты передачи данных по отдельным CardView
компонентам.
Чтобы включить это, откройте DeckView и добавьте следующее в верхней части структуры, прежде чемbody
:
`@ObservedObject var deck: FlashDeck
let onMemorized: () -> Void
init(deck: FlashDeck, onMemorized: @escaping () -> Void) {
self.onMemorized = onMemorized
self.deck = deck
}`
Вы добавляете FlashDeck
свойство для получения элементов, на которые будет подписываться представление , а также обратный onMemorized
вызов, когда пользователь запоминает карту. Оба они передаются через пользовательский инициализатор.
Чтобы предварительный просмотр по-прежнему работал, вам необходимо обновить DeckView_Previews``previews
его до следующего:
DeckView( deck: FlashDeck(from: ChallengesViewModel.challenges), onMemorized: {} )
И, наконец, внутри LearnView найдите DeckView()
в body
нем и замените его на:
DeckView( deck: learningStore.deck, onMemorized: { learningStore.score += 1 } )
Обратите внимание, как вы увеличиваете счет, когда пользователь запоминает карту. Пока нет способа запустить onMemorized
его , но вы добавите это позже в этой главе.
Далее, получение данных из хранилища обучения в отдельные карты. Для этого откройте CardView и добавьте в начало страницы следующее: body
:
`let flashCard: FlashCard
init(_ card: FlashCard) {
self.flashCard = card
}`
Здесь вы добавляете FlashCard
свойство в представление и передаете его через инициализатор. Свойство не является объектом состояния, потому что вы не планируете изменять его значение FlashCard
в любое время; данные карты фиксируются на весь срок службы объекта.
С реальной моделью карты вы также можете обновить body
представление, чтобы использовать его. Замените содержимое представления VStack
на:
Spacer() Text(flashCard.card.question) .font(.largeTitle) .foregroundColor(.white) Text(flashCard.card.answer) .font(.caption) .foregroundColor(.white) Spacer()
Здесь вы просто используете вопрос и ответ с флэш-карты вместо жестко закодированных значений.
С новым инициализатором вам нужно сделать обновление в тех местах, где CardView
он используется, а именно: CardView_Previews
и DeckView
.
Внутри CardView обновление CardView_Previews``previews
до:
let card = FlashCard( card: Challenge( question: "こんにちわ", pronunciation: "Konnichiwa", answer: "Hello" ) ) return CardView(card)
Затем, внутри DeckView, вам нужно будет изменить body
его для динамической поддержки нескольких CardView
s. Чтобы добавить поддержку нескольких CardView
s, сначала добавьте следующие вспомогательные методы в нижней части представления:
`func getCardView(for card: FlashCard) -> CardView {
let activeCards = deck.cards.filter { $0.isActive == true }
if let lastCard = activeCards.last {
if lastCard == card {
return createCardView(for: card)
}
}
let view = createCardView(for: card)
return view
}
func createCardView(for card: FlashCard) -> CardView {
let view = CardView(card)
return view
}`
Эти методы помогают создать a CardView
с помощью a FlashCard
.
Затем замените содержимое body
представления следующим:
ZStack { ForEach(deck.cards.filter { $0.isActive }) { card in getCardView(for: card) } }
Здесь ForEach
он берет все активные карты из колоды и создает CardView
их для каждой, используя только что созданные вспомогательные методы.
Глядя на холст для » или LearnView
«илиDeckView
«, вы должны увидеть такую карту:

Завершенная колода карт
Применение настроек
В предыдущей главе вы добавили два параметра, влияющих на раздел обучения:
- Learning Enabledв категории game используется для включения или отключения экрана learning.
- Цвет фона карты в категории внешний вид, используемый для персонализации фона карты.
Теперь пришло время пустить их в ход. Первое, что нужно сделать, это выставить оба параметра через UserDefaults, превратив их из @State
свойств в @AppStorage
свойства.
Первый очень прост: в SettingsView замените строку, где learningEnabled
объявлено с:
@AppStorage("learningEnabled") var learningEnabled: Bool = true
Что касается другого свойства, то оно Color
имеет тип, который не может обрабатывать UserDefaults, поэтому вы должны либо сделать его RawRepresentable
, либо использовать свойство shadow — подробнее об их различиях читайте в предыдущей главе.
Вы будете использовать последний метод, добавив свойство shadow Int
типа. Добавьте это свойство доcardBackgroundColor
:
@AppStorage("cardBackgroundColor") var cardBackgroundColorInt: Int = 0xFF0000FF
Затем body
добавьте новый onChange(of:perform)
модификатор в List
, сразу после двух других, которые заботятся о включении ежедневного напоминания и времени ежедневного напоминания:
.onChange(of: cardBackgroundColor, perform: { newValue in cardBackgroundColorInt = newValue.asRgba })
Наконец, в .onAppear
модификаторе инициализируйте цвет фона карты из свойства shadow — добавьте его после настройкиdailyReminderTime
:
cardBackgroundColor = Color(rgba: cardBackgroundColorInt)
После того как эти настройки будут скорректированы, вы должны использовать их соответствующим образом.
Вы используетеlearningEnabled
, чтобы включить или отключить раздел обучения, и самый простой способ добиться этого-показать или скрыть соответствующую вкладку.
Откройте HomeView и добавьте то же свойство AppStorage, что и определено вSettingsView
:
@AppStorage("learningEnabled") var learningEnabled: Bool = true
Затем окружите первую вкладку if
оператором, чтобы вкладка включалась только в том случае, если learningEnabled
она истинна:
if learningEnabled { LearnView() .tabItem({ VStack { Image(systemName: "bookmark") Text("Learn") } }) .tag(0) }
Теперь запустите приложение, перейдите в настройки, когда вы отключите обучение, вы увидите, что вкладка Обучение исчезает, тогда как если вы включите ее, она появится снова.

Настройки с отключенным обучением
Теперь , чтобы изменить цвет фона карты, добавьте соответствующее свойство CardView
в CardView:
@Binding var cardColor: Color
Вы объявляете его обязательным, потому что вы передадите его, так что источник истины определен в другом месте, а именно в DeckView
.
Возможно , у вас возникнет соблазн сделать это непосредственно внутриCardView
, но это будет неэффективно, потому что вы прочитаете одно и то же свойство из UserDefaults для каждой карты, тогда как, передавая его, DeckView
вы прочитаете его один раз и передадите одну и ту же привязку всем картам через их соответствующие инициализаторы.
Замените инициализатор CardView
’s для учета нового свойства:
init( _ card: FlashCard, cardColor: Binding<Color> ) { flashCard = card _cardColor = cardColor }
Затем замените статически установленный красный цвет фона значением вновь добавленного свойства. В операторе body
’s return``Rectangle
у представления есть .fill(Color.red)
модификатор — замените его на:
.fill(cardColor)
Наконец CardView
, вам нужно изменить предварительный просмотр, чтобы обработать дополнительный параметр. Заменить CardView_Previews
содержимое на:
`@State static var cardColor = Color.red
static var previews: some View {
let card = FlashCard(
card: Challenge(
question: «こんにちわ»,
pronunciation: «Konnichiwa»,
answer: «Hello»
)
)
return CardView(card, cardColor: $cardColor)
}`
Теперь откройте DeckView и добавьте это свойство:
@AppStorage("cardBackgroundColor") var cardBackgroundColorInt: Int = 0xFF0000FF
Вы будете использовать только свойство shadow вместо добавления второго свойства — вы преобразуете его Color
при передаче CardView
.
Затем замените createCardView(for:)
реализацию на:
`func createCardView(for card: FlashCard) -> CardView {
// 1
let view = CardView(card, cardColor: Binding(
get: { Color(rgba: cardBackgroundColorInt) },
set: { newValue in cardBackgroundColorInt = newValue.asRgba }
)
)
return view
}`
Здесь вы передали новый cardColor
параметр CardView
инициализатору, используя явную привязку. Теперь вы можете запустить приложение, снова включить обучение, если оно все еще было отключено, и выбрать цвет карты по вашему выбору — если вы активируете вкладку Обучение, вы увидите, что карты теперь отображаются с блестящим недавно выбранным цветом фона.

Выбор цвета фона карты
Твой первый жест
Примечание: если вы пропустили предыдущий раздел и сразу перешли к этому, откройте обновленный проект starter, который вы найдете в папке starter/starter-gestures.
Жесты в SwiftUI не так уж сильно отличаются от их собратьев в AppKit и UIKit, но они проще и несколько элегантнее, что дает некоторым разработчикам ощущение большей мощи.
Хотя они ничуть не лучше своих предшественников с точки зрения возможностей, их подход SwiftUI позволяет легче и убедительнее использовать жесты там, где раньше они часто были приятными для имущих.
Начав с основного жеста, пришло время вернуться CardView
к нему . Ранее вы добавили как исходное слово , так и переведенное словоCardView
, что несколько полезно. Но что делать, если пользователь хочет проверить свои знания, не получив немедленного ответа?
Было бы неплохо, если бы на карточке было оригинальное слово, а затем при необходимости можно было бы отобразить переведенное слово.
Чтобы добиться этого, вы можете добавить простой жест касания (буквально a TapGesture
) для этого взаимодействия. Краны вездесущи и необходимы, так что это отличное место, чтобы начать с жестов.
Начните с открытия CardView, затем добавьте следующее свойство, указывающее, был ли ответ обнаружен или нет, в верхнюю часть представления:
@State var revealed = false
Затем в поле body
добавить следующий .gesture
модификатор внизу, после.animation(_:)
:
.gesture(TapGesture() .onEnded { withAnimation(.easeIn, { revealed = !revealed }) })
Здесь вы используете готовый жест от Apple, который добавляет много удобства, имея дело с человеческими жестами касания последовательно во всех приложениях. onEnded
Блок позволяет вам предоставить дополнительный код для того, что происходит после завершения жеста касания. В этом случае вы предоставили анимацию, которая облегчает in (.easeIn
) revealed
инвертирование свойства.
В настоящее время инвертирование revealed
ничего не делает, но вы хотите, чтобы Text
отображение перевода отображалось только тогда, когда revealed
оно есть true
.
Чтобы добиться этого, внутри body
замените следующее:
Text(flashCard.card.answer) .font(.caption) .foregroundColor(.white)
С:
if revealed { Text(flashCard.card.answer) .font(.caption) .foregroundColor(.white) }
Попробуйте предварительно просмотреть приложение на холсте с помощью Live Preview и нажать на карточку. Вы должны увидеть довольно плавную и приятную легкую анимацию для переведенного слова. Это так же просто, как жесты, и с помощью анимационных блоков он обеспечивает уровень текучести и утонченности, который оценят пользователи.

Поток жестов крана
Также обратите внимание, что нажатие на карту несколько раз подряд все равно даст плавную анимацию.
Легко, правда?
Пользовательские жесты
Хотя жест касания и другие простые жесты обеспечивают большой пробег для взаимодействия, часто бывают случаи, когда более сложные жесты являются достойным дополнением, обеспечивая большее чувство утонченности среди потока приложений, доступных в App Store.
Для этого приложения вам все равно нужно предоставить пользователю возможность заявить, запомнил ли он карту или нет. Вы можете сделать это, добавив пользовательский жест перетаскивания и оценив результат в зависимости от направления перетаскивания. Это гораздо сложнее, чем простой жест касания, но, благодаря элегантности SwiftUI, это все еще довольно безболезненно по сравнению с предыдущими методами достижения той же цели.
Первым шагом является добавление перечисления, которое обозначает направление, в котором сбрасывается карта. В DeckView добавьте следующий код передDeckView
:
enum DiscardedDirection { case left case right }
Вы можете определить более сложные метрики для этого взаимодействия (вверх, вниз, …), но это представление должно понимать только два потенциальных варианта.
Далее, пришло время сделать карты перетаскиваемыми! В CardView добавьте новое typealias
свойство и в верхнюю часть представления, чуть ниже revealed
свойства:
`typealias CardDrag = (_ card: FlashCard,
_ direction: DiscardedDirection) -> Void
let dragged: CardDrag`
При вызове dragged
это свойство принимает перетаскиваемую карту и результат перечисления , в каком направлении она была перетащена.
Затем обновитеinit
, чтобы принять перетаскиваемое закрытие в качестве параметра:
init( _ card: FlashCard, cardColor: Binding<Color>, onDrag dragged: @escaping CardDrag = {_,_ in } ) { flashCard = card _cardColor = cardColor self.dragged = dragged }
Далее вам нужно изменить DeckView
его таким образом, чтобы он поддерживал функциональность новой карты. Откройте DeckView и замените реализацию createCardView(for:)
на следующую:
`func createCardView(for card: FlashCard) -> CardView {
let view = CardView(card, cardColor: Binding(
get: { Color(rgba: cardBackgroundColorInt) },
set: { newValue in cardBackgroundColorInt = newValue.asRgba }
),
onDrag: { card, direction in
if direction == .left {
onMemorized()
}
}
)
return view
}`
Здесь вы добавляете onDrag
обратный вызов в CardView
экземпляр.
Если направление перетаскивания таково .left
, вы запускаете триггер onMemorized()
, и счетчик in LearningStore
будет увеличен на единицу — это потому, что при создании экземпляра DeckView
from LearView
вы передали закрытие для onMemorized
параметра, который это делает:
DeckView( deck: learningStore.deck, onMemorized: { learningStore.score += 1 } )
Последний шаг — добавить фактический жест перетаскивания. Вернитесь в CardView, затем добавьте следующее свойство послеrevealed
:
@State var offset: CGSize = .zero
Вы будете использовать это смещение, чтобы переместить карту в новое положение.
Далее создаем жест перетаскивания. В верхней части body
изменить строку:
ZStack {
в:
return ZStack {
Вам нужно return``ZStack
добавить настройку жеста перетаскивания над ним. Прямо над этой строкой кода, все еще находясь внутри тела, добавьте следующее::
let drag = DragGesture() // 1 .onChanged { offset = $0.translation } // 2 .onEnded { if $0.translation.width < -100 { offset = .init(width: -1000, height: 0) dragged(flashCard, .left) } else if $0.translation.width > 100 { offset = .init(width: 1000, height: 0) dragged(flashCard, .right) } else { offset = .zero } }
Это DragGesture
делает большую часть работы за вас, но есть несколько вещей, которые стоит отметить:
- При каждом движении, записанном во время перетаскивания,
onChanged
будет происходить событие. Вы изменяетеoffset
свойство (которое является объектом координат x и y), чтобы оно соответствовало движению перетаскивания пользователя. Например, если пользователь начал перетаскивать в координатном пространстве (0, 0), аonChanged
триггер сработал, когда пользователь все еще перетаскивал в координатном пространстве (200, -100), то смещение оси x было бы увеличено на 200, а смещение оси y уменьшилось бы на 100. По сути, это означает, что компонент будет двигаться вправо и вверх по экрану в соответствии с движением пальца пользователя. - Это
onEnded
событие происходит, когда пользователь прекращает перетаскивание, обычно когда его палец убирается с экрана. На этом этапе вы хотите определить, в каком направлении пользователь перетащил карту и перетащил ли он ее достаточно далеко, чтобы считаться решением (в этот момент вы записываете решение и отбрасываете карту), или считаете ли вы его еще нерешенным (в этот момент вы сбрасываете карту в исходные координаты).. Вы используете -100 и 100 в качестве маркеров решения о том, выбрал ли пользователь влево или вправо во время перетаскивания, и это решение передается вdragged
закрытие.
Это все, что вам нужно для жеста перетаскивания. Теперь вам просто нужно добавить его в body
качестве модификатора вместе с ранее определенным offset
. Прямо над .gesture(TapGesture()
этим добавьте:
.offset(offset) .gesture(drag)
Жест перетаскивания может быть передан в метод gesture в качестве параметра, и вы должны видеть, что жест касания-это просто еще один жест, добавленный к объекту: нет никакого конфликта с включением нескольких жестов и укладкой их в объект, если это необходимо.
Также есть пружинная анимация, чтобы карта плавно возвращалась в исходное положение, но для правильной работы требуется небольшая регулировка. В настоящее время он указан как:
.animation(.spring(), value: 0)
Но value
параметр должен быть значением, которое отслеживается на предмет изменений, так что анимация выполняется только при изменении этого значения. Поскольку вы используете offset
для вычисления положения карты именно это значение. Изменить следующим образом:
.animation(.spring(), value: offset)
Теперь вы можете строить и запускать, чтобы проверить свой прогресс. Теперь вы можете перетащить карту и провести пальцем влево и вправо.
Вы также можете попробовать предварительный просмотр LearnView
с помощью Live Preview и увидеть жест перетаскивания в действии.

Жест перетаскивания карты
Но что делать, если вы хотите объединить жесты?
Комбинирование жестов для более сложных взаимодействий
Возможно, вы хотите предоставить пользователю элегантный визуальный индикатор, если он выбирает карту достаточно долго, чтобы понять, что существует дальнейшее взаимодействие. При удержании нажатой кнопки объекты часто могут подпрыгивать или выскакивать из своего положения, давая немедленную визуальную подсказку о том, что объект может быть перемещен.
SwiftUI предоставляет возможность добавить такое изменение, объединив два жеста. При объединении жестов SwiftUI предоставляет несколько вариантов их взаимодействия:
- Последовательность: жест, который следует за другим жестом.
- Одновременные: жесты, которые активны одновременно.
- Эксклюзив: жесты, которые могут быть добавлены одновременно, но только один может быть активен одновременно.
В этом случае вы собираетесь добавить одновременный жест, потому что хотите дать простой ключ к потенциалу возможного жеста перетаскивания, не препятствуя одновременному вызову жеста перетаскивания.
Это может показаться сложным, но, как вы увидите, это невероятно просто.
Во-первых, добавьте новое свойство для хранения состояния жеста перетаскивания.CardView
:
@GestureState var isLongPressed = false
Вы заметите новый атрибут состояния @GestureState
. Этот атрибут позволяет сохранять и считывать состояние жеста во время жеста, чтобы влиять на эффекты, которые этот жест может оказывать на чертеж вида.
Это свойство будет использоваться для записи того, была ли карта нажата в течение длительного времени или нет, и автоматически сбрасывается по завершении жеста. Если вместо этого вы используете @State
свойство, оно не будет сброшено по окончании жеста.
Затем, в верхней части body
экрана , прямо под настройкой drag
, добавьте новый жест для длительного нажатия:
let longPress = LongPressGesture() .updating($isLongPressed) { value, state, transition in state = value } .simultaneously(with: drag)
Обратите внимание, как вы создаете новый жест и комбинируете его одновременно с другим жестом drag
.
Этот жест LongPressGesture
— еще один последовательный жест, предоставленный Apple. В нем вы используете updating
тело для привязки значения к состоянию, а затем добавляете предыдущий жест перетаскивания в качестве потенциального одновременного жеста.
Чтобы увидеть его в действии, в нижней части body
замените ранее созданный жест перетаскивания:
.gesture(drag)
С:
.gesture(longPress) .scaleEffect(isLongPressed ? 1.1 : 1)
Обратите внимание, что вы также добавили scaleEffect
модификатор, чтобы увеличить масштаб представления на 10%, если это isLongPressed
свойство есть true
.
Попробуйте это сделать, LearnView
предварительно просмотрев или запустив приложение в симуляторе. Теперь вы должны иметь возможность нажимать на карту и видеть, как она масштабируется, в то же время имея возможность перетаскивать ее влево или вправо.
Вы можете заметить, что к этому импульсному эффекту не применяется анимация, но это легко исправить. Просто добавьте для него анимацию, связанную с isLongPressed
объектом после.scaleEffect(:_)
:
.animation( .easeInOut(duration: 0.3), value: isLongPressed )
Теперь, если вы запустите или просмотрите еще раз, вы увидите, что анимация применена!
Это простой, но эффективный одновременный комбинированный жест, написанный с помощью всего лишь нескольких кодов и простого модификатора жестов. Отличная работа!
Однако вы можете видеть, что жест tap для отображения перевода, который вы добавили ранее, больше не работает: если вы нажмете на карту, ничего не произойдет — это происходит потому, что жест длительного нажатия скрывает ее.
Быстрый способ исправить это — использовать .simultaneousGesture()
. Заменить
.gesture(TapGesture() ... )
С:
.simultaneousGesture(TapGesture() ... )
И нажмите, чтобы показать жест перевода, снова заработает!
Ключевые моменты
Жесты-это прекрасный способ превратить базовое приложение в приятный и интуитивно понятный пользовательский интерфейс, и SwiftUI добавила мощные модификаторы, чтобы сделать его простым и эффективным в любом приложении, которое вы пишете. В этой главе вы узнали:
- Как создавать простые жесты из встроенной библиотеки Apple. Просто используйте
gesture
модификатор вместе с жестом для использования. - Как создать пользовательские жесты для более уникальных взаимодействий.
- Как сочетать анимацию и жесты для более плавного восприятия.
Куда идти дальше?
Вы многое сделали с помощью жестов, но есть гораздо больше возможностей. Проверьте следующий ресурс для получения дополнительной информации о том, куда идти отсюда: SwiftUI gesture documentation: apple.co/3cBuVgd