Для фигуры, которую перетаскивают в вашем приложении для Mac, напрямую обновляйте её смещение во время жеста, а при отпускании анимируйте то же смещение с помощью пружины. В macOS 14 и новее SwiftUI может автоматически передавать в пружину отслеживаемую скорость перетаскивания.
В этом руководстве создаётся простое взаимодействие для настольного приложения: перетаскивайте синюю синтетическую плитку в любом направлении, отпустите её и позвольте ей вернуться пружинным движением в исходное положение. Всё состояние хранится в памяти, а жест обрабатывается самой плиткой в собственном окне приложения.
Как пружина наследует конечную скорость перетаскивания?
Transaction.tracksVelocity в SwiftUI записывает скорость по мере изменения анимируемых свойств. Согласно документации Apple, обратные вызовы жестов onChanged и updating автоматически включают это отслеживание. По завершении взаимодействия анимация может использовать накопленную скорость. Поэтому для такого обычного взаимодействия SwiftUI вам не нужно вычислять и передавать начальную скорость. Документация Apple об отслеживании скорости прямо описывает эту передачу.
Важно связать анимацию с нужным свойством. Во время перетаскивания записывайте смещение жеста в смещение плитки. При отпускании измените то же смещение на конечное значение внутри withAnimation. Если представление и его состояние остаются активными, SwiftUI сможет непрерывно отслеживать это свойство.
В этой реализации не анимируйте обновления при перетаскивании. Apple поясняет, что отслеживание скорости транзакцией и анимация, самостоятельно управляющая своей скоростью, несовместимы. Можно также использовать интерактивную пружину во время перетаскивания, но тогда плитка будет иначе следовать за указателем; для прямого следования, показанного здесь, это не требуется. Отслеживание скорости транзакцией
Конечное положение пружины и начальное движение выполняют разные задачи. Конечное положение указывает, где должна остановиться плитка. Унаследованная скорость показывает, как она двигалась в момент отпускания. Если отпустить плитку, когда она движется от исходного положения, она может сначала продолжить движение наружу, а затем пружина вернёт её обратно. Apple показывает, как пружины продолжают движение с учётом скорости жеста, в статье Анимация с помощью пружин.
Создайте полноценный пример перетаскивания и отпускания для macOS
Создайте в Xcode приложение macOS на SwiftUI, задайте целевую версию macOS 14.0 или новее и замените сгенерированный файл точки входа приложения кодом ниже. В целевом приложении должна быть только одна декларация приложения @main. В примере используется API завершения анимации из macOS 14. Документация Apple о завершении анимации
Это полноценное одnofайловое приложение без ресурсов и зависимостей пакетов. Сигнатуры API сверены с документацией; в этой исследовательской среде настольное приложение не собиралось и не запускалось.
import SwiftUI
@main
struct SpringReleaseDemoApp: App {
var body: some Scene {
WindowGroup {
SpringReleaseView()
}
}
}
struct SpringReleaseView: View {
@State private var offset: CGSize = .zero
@State private var isReturning = false
@GestureState private var isDragging = false
var body: some View {
VStack(spacing: 16) {
Text("Drag the tile, then release")
.font(.headline)
ZStack {
RoundedRectangle(cornerRadius: 20)
.stroke(.gray.opacity(0.4), lineWidth: 2)
.frame(width: 112, height: 112)
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
.frame(width: 112, height: 112)
.contentShape(Rectangle())
.offset(offset)
.gesture(tileDrag)
.allowsHitTesting(!isReturning)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.coordinateSpace(name: "canvas")
}
.padding(24)
.frame(width: 640, height: 440)
.onChange(of: isDragging) { _, active in
guard !active, !isReturning else { return }
// Cancellation cleanup has no successful release value.
withTransaction(Transaction(animation: nil)) {
offset = .zero
}
}
}
private var tileDrag: some Gesture {
DragGesture(
minimumDistance: 3,
coordinateSpace: .named("canvas")
)
.updating($isDragging) { _, active, _ in
active = true
}
.onChanged { value in
// Gesture callbacks automatically track velocity.
offset = value.translation
}
.onEnded { _ in
returnHome()
}
}
private func returnHome() {
guard !isReturning else { return }
isReturning = true
withAnimation(
.spring(duration: 0.45, bounce: 0.2),
completionCriteria: .removed
) {
offset = .zero
} completion: {
isReturning = false
}
}
}
Жест привязан к движущейся плитке, а именованное пространство координат принадлежит неподвижному холсту. Так выбор системы координат для взаимодействия становится явным. В документации Apple по DragGesture описано добавление настроенного перетаскивания с помощью модификатора жеста.
value.translation — это полное смещение с момента начала перетаскивания. Присвойте его offset; не прибавляйте его снова и снова к предыдущему смещению. При повторном сложении одно и то же движение учитывалось бы несколько раз. В этом примере каждое принятое перетаскивание начинается из исходного положения, поэтому отдельное начальное смещение не нужно. Определение смещения в документации Apple
При успешном отпускании немедленно вызывается returnHome(). Смещение предварительно не сбрасывается, и пружина не запускается с задержкой. Благодаря этому отслеживаемые изменения во время перетаскивания и анимация при отпускании остаются связанными через одно и то же свойство состояния.
Зачем отдельно хранить состояние перетаскивания, положения и возврата?
Эти три свойства отвечают на разные вопросы: offset хранит заданное смещение, isDragging указывает, активен ли жест, а isReturning отмечает фазу возврата в этом примере. Эти значения локально хранит владелец плитки. Apple рекомендует объявлять состояние, принадлежащее представлению, как приватное и передавать привязки только туда, где другому представлению нужно его изменять. Документация по состоянию
Синтаксис обёртки также используется с определённой целью. Обёртки свойств Swift управляют доступом к обёрнутому значению и могут предоставлять проецируемое значение с префиксом в виде знака доллара. Здесь $isDragging предоставляет проекцию состояния жеста, используемую в updating; это не второй флаг, который поддерживается независимо. Язык программирования Swift: свойства
Отмену нужно обрабатывать отдельно, поскольку onEnded вызывается только при успешном завершении жеста. Apple рекомендует @GestureState для состояния, которое должно сбрасываться независимо от способа завершения жеста. В примере отслеживается сброс этого состояния, и синтетическая плитка возвращается на место, если обычный возврат ещё не выполняется. В таком случае она мгновенно перемещается в исходное положение; здесь не предполагается, что у отменённого жеста есть конечная скорость отпускания. Завершение жеста, GestureState
В примере также предусмотрено, что новое перетаскивание принимается только после завершения анимации возврата. Когда смещение модели устанавливается в ноль, анимация уже запускается, поэтому ноль становится заданным конечным положением, пока плитка ещё видимо движется. Если просто убрать это ограничение, потребуется продумать, как новое перетаскивание продолжится от текущего отображаемого положения.
allowsHitTesting(!isReturning) определяет, участвует ли плитка в проверке попадания. Критерий завершения .removed снимает ограничение, когда вся анимация завершена. Это позволяет не угадывать задержку по параметру длительности пружины. Управление проверкой попадания, Завершение с удалением анимации
Как настроить и проверить возврат при отпускании?
Сначала зафиксируйте конечное положение и параметры пружины. Прежде чем менять параметры анимации, измените способ отпускания плитки. Так можно отделить передачу скорости от настройки пружины.
Проведите следующие проверки в окне приложения. Это диагностическая последовательность, основанная на описанном в документации механизме передачи скорости, а не на измерениях этого примера.
| Проверка | Действие | На что обратить внимание |
|---|---|---|
| Отпускание почти без движения | Перетащите плитку из исходного положения, остановитесь, затем отпустите. | Возврат должен начаться почти без унаследованного движения. |
| Отпускание при движении наружу | Перетащите плитку из исходного положения и отпустите, пока она ещё движется наружу. | Пружина должна сначала продолжить движение наружу, а затем развернуть его. |
| Отпускание при движении к исходному положению | Перетащите плитку, разверните её к исходному положению, затем отпустите. | Возврат должен продолжить движение к исходному положению. |
| Диагональное отпускание | Отпустите плитку, когда она движется по обеим осям. | Убедитесь, что сохраняется направление движения, а возврат не происходит только по горизонтали. |
| Следующее взаимодействие | Дождитесь завершения возврата, затем снова перетащите плитку. | Новое перетаскивание должно начинаться из исходного положения. |
Настройте duration, чтобы изменить темп, и bounce, чтобы изменить характер движения. Значения 0.45 и 0.2 в примере служат отправными точками и не являются предписанными настройками. Apple определяет длительность как воспринимаемую длительность и описывает нулевой отскок как критическое затухание; положительные значения отскока дают всё более выраженные колебания. Настройка пружины
Воспринимаемая длительность не гарантирует точного момента, когда завершится каждое видимое движение. Apple отличает её от длительности затухания, которая зависит от движения пружины. В примере используйте обратный вызов завершения для фазы возврата и не рассматривайте 0.45 как таймер. Анимация с помощью пружин
Если кажется, что при отпускании движение игнорируется, сначала проверьте передачу: изменяет ли жест то же смещение, которое анимирует пружина? Перезаписывает ли другой код это смещение до отпускания? Не заменяется ли представление? Не анимирует ли модификатор анимации обновления во время перетаскивания? Эти проверки напрямую следуют из документированного требования отслеживать изменения анимируемых свойств до передачи управления анимации.
Когда нужно явно считывать скорость или использовать AppKit?
DragGesture.Value.velocity предоставляет текущую скорость перетаскивания, а onEnded — конечное значение жеста после успешного завершения. Считывать его полезно, когда скорость влияет на решение приложения, например на выбор одного из двух синтетических положений привязки. Для фиксированного положения возврата в этом примере автоматическое отслеживание уже обеспечивает передачу данных анимации. Скорость перетаскивания, Конечное значение жеста
Если вы выбрали API начальной скорости пружины с явным заданием значения, проверьте, в каком формате она передаётся, прежде чем подставлять измерение жеста. В демонстрации Apple для анимации под управлением жеста скорость преобразуется в единичную скорость делением на оставшееся расстояние до цели. В качестве примера такой расчёт превратил бы 300 точек в секунду на расстоянии 150 точек до цели в 2 расстояний анимации в секунду. При этом также нужна чёткая политика на случай, когда оставшееся расстояние равно нулю. Используемый здесь автоматический механизм SwiftUI избавляет от этого расчёта. Улучшение анимации интерфейса и переходов
Используйте AppKit, когда взаимодействием должно управлять уже существующее представление или распознаватель жестов AppKit, принадлежащий приложению. NSPanGestureRecognizer.velocity(in:) возвращает горизонтальную и вертикальную скорость в точках в секунду относительно указанного представления. Хотя сигнатура метода Swift допускает необязательное представление, Apple прямо указывает, что аргумент не должен быть nil. Скорость жеста панорамирования в AppKit
В macOS 26 и новее документированным способом интеграции распознавателя жестов AppKit в иерархию SwiftUI служит NSGestureRecognizerRepresentable. SwiftUI управляет его созданием и обновлениями и предоставляет поддержку преобразования координат. Apple предупреждает, что геометрия базового представления распознавателя может отличаться от геометрии представления SwiftUI, поэтому при переходе между ними используйте конвертер представления-обёртки. Интеграция жестов AppKit
Для взаимодействия с плиткой оставьте жест SwiftUI единственным владельцем обновлений перетаскивания и позвольте пружине при отпускании анимировать уже используемое смещение. Когда эта передача заработает, настройте конечное положение и параметры пружины, сохранив те же границы состояния и системы координат.
Источники
tracksVelocity — Apple Developer
Анимация с помощью пружин — Apple Developer
withAnimation(:completionCriteria::completion:) — Apple Developer
DragGesture — Apple Developer
translation — Apple Developer
State — Apple Developer
Properties — Swift.org
onEnded(_:) — Apple Developer
GestureState — Apple Developer
allowsHitTesting(_:) — Apple Developer
removed — Apple Developer
spring(duration:bounce:blendDuration:) — Apple Developer
velocity — Apple Developer
Улучшение анимации интерфейса и переходов — Apple Developer
velocity(in:) — Apple Developer
NSGestureRecognizerRepresentable — Apple Developer