Untuk shape yang diseret di dalam app Mac Anda, perbarui offset-nya secara langsung selama gestur, lalu animasikan offset yang sama dengan spring saat dilepas. Di macOS 14 dan versi lebih baru, SwiftUI dapat meneruskan kecepatan seret yang dilacak ke spring secara otomatis.
Tutorial ini membuat interaksi desktop sederhana: seret tile biru sintetis ke arah mana pun, lepaskan, lalu biarkan spring mengembalikannya ke posisi awal. Semua state tetap berada di memori, dan gestur melekat pada tile di jendela app itu sendiri.
Bagaimana spring mewarisi kecepatan akhir seretan?
Transaction.tracksVelocity milik SwiftUI merekam kecepatan saat properti yang dapat dianimasikan berubah. Apple menjelaskan bahwa callback gestur onChanged dan updating secara otomatis mengaktifkan pelacakan ini. Saat interaksi berakhir, animasi dapat menggunakan kecepatan yang terkumpul. Jadi, Anda tidak perlu menghitung dan meneruskan kecepatan awal untuk interaksi SwiftUI biasa ini. Dokumentasi pelacakan kecepatan Apple menjelaskan serah terima ini secara eksplisit.
Kaitan yang penting adalah properti yang dianimasikan. Selama penyeretan, tuliskan translasi gestur ke offset tile. Saat dilepas, ubah offset yang sama ke tujuan di dalam withAnimation. Dengan mempertahankan view dan state-nya, SwiftUI memiliki properti berkelanjutan untuk dilacak.
Dalam implementasi ini, jangan animasikan pembaruan selama penyeretan. Apple menjelaskan bahwa pelacakan kecepatan transaksi dan animasi yang mengelola kecepatannya sendiri tidak dapat digunakan bersamaan. Spring interaktif selama penyeretan adalah teknik lain yang didukung, tetapi teknik itu mengubah cara tile mengikuti penunjuk; teknik tersebut tidak diperlukan untuk interaksi yang mengikuti penunjuk secara langsung di sini. Pelacakan kecepatan transaksi
Tujuan spring dan gerakan awalnya memiliki fungsi yang berbeda. Tujuan menentukan tempat tile akan berhenti. Kecepatan yang diwarisi menentukan gerakannya saat dilepas. Tile yang dilepas saat bergerak menjauh dari posisi awal dapat terus bergerak ke luar sejenak sebelum spring mengembalikannya. Apple menunjukkan spring yang meneruskan gerakan dari kecepatan gestur dalam Menganimasikan dengan spring.
Buat contoh lengkap penyeretan dan pelepasan di macOS
Buat app macOS di Xcode menggunakan SwiftUI, atur target deployment ke macOS 14.0 atau versi lebih baru, lalu ganti file titik masuk app yang dihasilkan dengan kode berikut. Pastikan hanya ada satu deklarasi app @main di target. Contoh ini menggunakan API penyelesaian animasi macOS 14. Dokumentasi penyelesaian animasi Apple
Ini adalah aplikasi lengkap dalam satu file tanpa aset atau dependensi paket. Tanda tangan API telah diperiksa terhadap dokumentasi; aplikasi desktop tidak dikompilasi atau dijalankan di lingkungan riset ini.
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
}
}
}
Gestur dipasang pada tile yang bergerak, sedangkan ruang koordinat bernama dimiliki oleh kanvas yang diam. Dengan demikian, pilihan koordinat interaksi dinyatakan secara eksplisit. Dokumentasi DragGesture dari Apple menjelaskan pemasangan seret yang telah dikonfigurasi dengan modifier gestur.
value.translation adalah total perpindahan sejak seretan dimulai. Tetapkan nilainya ke offset; jangan terus menambahkannya ke offset sebelumnya. Penambahan berulang akan mengakumulasi gerakan yang sama berkali-kali. Dalam contoh ini, setiap seretan yang diterima dimulai dari posisi awal, jadi tidak diperlukan offset awal terpisah. Definisi translasi Apple
Pelepasan yang berhasil langsung memanggil returnHome(). Offset tidak diatur ulang terlebih dahulu dan spring juga tidak dijadwalkan untuk nanti. Dengan begitu, perubahan seretan yang dilacak dan animasi pelepasan tetap terhubung melalui properti state yang sama.
Mengapa state seret, posisi, dan kembali perlu dipisahkan?
Ketiga properti menjawab pertanyaan yang berbeda: offset menyimpan perpindahan yang diminta, isDragging melacak gestur yang sedang berlangsung, dan isReturning mencatat fase pengembalian dalam contoh. Pemilik tile menyimpan nilai-nilai ini secara lokal. Apple menyarankan agar state yang dimiliki view dideklarasikan sebagai private dan binding dibagikan hanya saat view lain perlu mengubahnya. Dokumentasi State
Sintaks wrapper juga memiliki tujuan khusus. Wrapper properti mengelola akses ke nilai yang dibungkus dan dapat menyediakan nilai proyeksi dengan prefiks tanda dolar. Di sini, $isDragging menyediakan proyeksi gesture-state yang digunakan oleh updating; ini bukan flag kedua yang dikelola secara terpisah. The Swift Programming Language: Properties
Pembatalan perlu ditangani secara terpisah karena onEnded hanya dijalankan saat gestur selesai dengan sukses. Apple menyarankan @GestureState untuk state yang harus direset terlepas dari cara gestur berakhir. Contoh ini mengamati reset tersebut dan mengembalikan tile sintetis jika proses pengembalian normal tidak sedang berlangsung. Mekanisme cadangan ini langsung mengembalikan tile ke posisi awal; mekanisme ini tidak mengklaim memiliki kecepatan pelepasan akhir untuk gestur yang dibatalkan. Perilaku penyelesaian gestur, GestureState
Contoh ini juga sengaja hanya menerima seretan baru setelah animasi pengembalian selesai. Mengatur offset model ke nol akan memulai animasi, jadi nol sudah menjadi tujuan yang diminta meskipun tile masih terlihat bergerak. Jika pembatasan ini dihapus, Anda perlu merancang cara seretan baru mengambil alih dari posisi yang sedang ditampilkan.
allowsHitTesting(!isReturning) mengatur apakah tile ikut dalam pengujian hit. Kriteria penyelesaian .removed menghapus pembatasan setelah seluruh animasi selesai. Dengan begitu, Anda tidak perlu menebak jeda berdasarkan parameter durasi spring. Kontrol pengujian hit, Penyelesaian penghapusan animasi
Bagaimana sebaiknya Anda menyesuaikan dan memeriksa pelepasan?
Mulailah dengan tujuan dan pengaturan spring yang tetap. Ubah cara Anda melepas tile sebelum mengubah parameter animasi. Dengan begitu, pewarisan kecepatan dapat dipisahkan dari penyetelan spring.
Lakukan pemeriksaan berikut di dalam jendela. Ini adalah urutan diagnostik berdasarkan serah terima yang dijelaskan dalam dokumentasi, bukan hasil pengukuran dari contoh ini.
| Pemeriksaan | Tindakan | Hal yang perlu diperhatikan |
|---|---|---|
| Pelepasan hampir diam | Seret menjauh dari posisi awal, berhenti sejenak, lalu lepaskan. | Gerakan kembali seharusnya dimulai dengan sedikit gerakan yang terbawa. |
| Pelepasan ke arah luar | Seret menjauh dari posisi awal, lalu lepaskan saat masih bergerak ke luar. | Spring seharusnya meneruskan gerakan ke luar sebelum berbalik. |
| Pelepasan ke arah dalam | Seret menjauh, berbalik ke arah posisi awal, lalu lepaskan. | Gerakan kembali seharusnya meneruskan gerakan ke dalam. |
| Pelepasan diagonal | Lepaskan saat bergerak pada kedua sumbu. | Pastikan serah terima mempertahankan arahnya, alih-alih hanya berperilaku seperti gerakan kembali horizontal. |
| Interaksi berikutnya | Tunggu hingga gerakan kembali selesai, lalu seret lagi. | Seretan baru seharusnya dimulai dari posisi awal. |
Sesuaikan duration untuk mengatur tempo dan bounce untuk mengatur karakter gerakan. Nilai 0.45 dan 0.2 dalam contoh ini hanyalah nilai awal ilustratif, bukan pengaturan yang diwajibkan. Apple mendefinisikan durasi sebagai durasi perseptual dan menjelaskan bahwa pantulan nol bersifat redaman kritis, sedangkan nilai pantulan positif menghasilkan gerakan spring yang makin memantul. Konfigurasi spring
Durasi perseptual bukan jaminan pasti tentang kapan setiap gerakan yang terlihat akan berakhir. Apple membedakannya dari durasi hingga mencapai keadaan stabil, yang bergantung pada gerakan spring. Untuk fase pengembalian contoh ini, gunakan callback penyelesaian alih-alih menganggap 0.45 sebagai timer. Menganimasikan dengan spring
Jika pelepasan tampak mengabaikan gerakan, periksa dulu serah terimanya: apakah gestur mengubah offset yang sama dengan yang dianimasikan spring? Apakah kode lain menimpa offset tersebut sebelum pelepasan? Apakah view diganti? Apakah modifier animasi menganimasikan pembaruan saat penyeretan? Pemeriksaan ini langsung mengikuti persyaratan yang dijelaskan dalam dokumentasi: lacak properti yang dapat dianimasikan saat berubah sebelum meneruskannya ke animasi.
Kapan sebaiknya Anda membaca kecepatan secara eksplisit atau menggunakan AppKit?
DragGesture.Value.velocity menyediakan kecepatan seret saat ini, dan onEnded memberikan nilai akhir gestur setelah penyelesaian yang berhasil. Membacanya berguna ketika kecepatan memengaruhi keputusan aplikasi, misalnya memilih di antara dua lokasi snap sintetis. Untuk tujuan pengembalian tetap dalam contoh ini, pelacakan otomatis sudah menyediakan serah terima ke animasi. Kecepatan seret, Nilai akhir gestur
Jika Anda memilih API kecepatan awal spring eksplisit, periksa representasinya sebelum meneruskan hasil pengukuran gestur. Demonstrasi animasi berbasis gestur dari Apple mengonversi kecepatan menjadi kecepatan satuan dengan membagi kecepatan dengan jarak perjalanan yang tersisa. Perhitungan ilustratifnya akan mengubah 300 poin per detik selama perjalanan 150 poin menjadi 2 jarak animasi per detik. Konversi ini juga memerlukan kebijakan yang jelas saat jarak yang tersisa bernilai nol. Jalur SwiftUI otomatis menghindari perhitungan ini di sini. Sempurnakan animasi dan transisi UI Anda
Gunakan AppKit saat interaksi perlu dimiliki oleh view atau recognizer AppKit yang sudah menjadi bagian dari app. NSPanGestureRecognizer.velocity(in:) mengembalikan kecepatan horizontal dan vertikal dalam poin per detik relatif terhadap view yang diberikan. Meskipun tanda tangan Swift-nya menerima view opsional, Apple secara eksplisit menyatakan bahwa argumen tersebut tidak boleh berupa nil. Kecepatan pan AppKit
Untuk macOS 26 dan versi lebih baru, NSGestureRecognizerRepresentable adalah cara yang didokumentasikan untuk mengintegrasikan gesture recognizer AppKit ke dalam hierarki SwiftUI. SwiftUI mengelola pembuatan dan pembaruannya serta menyediakan dukungan konversi koordinat. Apple mengingatkan bahwa geometri view yang mendasari recognizer dapat berbeda dari geometri view SwiftUI, jadi gunakan konverter milik representable saat melewati batas tersebut. Integrasi gestur AppKit
Untuk interaksi tile ini, jadikan gestur SwiftUI satu-satunya pemilik pembaruan seret dan biarkan spring pelepasan menganimasikan offset yang sudah ada. Setelah serah terima ini berfungsi, sesuaikan tujuan dan parameter spring sambil mempertahankan state dan batas koordinat yang sama.
Sumber
tracksVelocity — Apple Developer
Menganimasikan dengan spring — 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
Sempurnakan animasi dan transisi UI Anda — Apple Developer
velocity(in:) — Apple Developer
NSGestureRecognizerRepresentable — Apple Developer