Pasang seretan induk dengan simultaneousGesture(_:including:) agar seretan itu dapat berpartisipasi bersamaan dengan interaksi anak. Pertahankan tindakan klik di tombol anak, tetapkan ambang gerakan untuk menyeret, dan tentukan secara eksplisit apakah pemilihan boleh terjadi bersamaan dengan penyeretan.
Panduan ini membuat panel yang dapat dipindahkan dan berisi tiga tombol item sintetis. Tugas pembaca spesifik: mengeklik item akan memilihnya, sedangkan menyeret panel mengubah posisinya di dalam app Mac yang sama. Contoh ini memungkinkan pemilihan dan perpindahan terjadi bersamaan; variasi dengan handel terpisah menyediakan area interaksi tersendiri jika itu lebih sesuai.
Tentukan kontrak interaksi sebelum mengubah prioritas gestur
Apple mendokumentasikan simultaneousGesture(_:including:) sebagai cara menambahkan gestur dengan prioritas yang sama seperti gestur yang sudah ada. Contohnya memperlihatkan penangan ketukan anak dan penangan ketukan kontainer yang membungkusnya sama-sama merespons klik. Ini adalah mekanisme komposisi yang relevan ketika induk perlu berpartisipasi tanpa sengaja mendahului anak. Dokumentasi Apple tentang gestur simultan
Namun, pengenalan simultan tidak dengan sendirinya menyatakan aturan “penyeretan harus membatalkan setiap tindakan anak.” Contoh Apple juga tidak membuktikan bahwa setiap gaya tombol dan setiap kontainer mengatur interaksi tombol dan seretan dengan cara yang sama. Perlakukan partisipasi gestur dan perilaku aplikasi sebagai dua keputusan terpisah.
Untuk panel ini, induk memiliki offset panel dan item yang dipilih. Setiap anak memiliki tombol yang meminta pemilihan melalui closure. Permintaan pemilihan tetap diterima meskipun interaksi penunjuk yang sama juga memindahkan panel. Jika perilaku yang Anda inginkan melarang aktivasi selama panel bergerak, gunakan variasi handel terpisah di bawah ini, alih-alih bergantung pada urutan callback yang tidak didokumentasikan.
Gunakan Button untuk tindakan anak. Apple secara eksplisit menyarankan tombol ketika interaksi memerlukan perilaku tombol, termasuk perilaku aksesibilitas standar. Shape kustom dapat tetap menjadi label tombol; shape tersebut tidak perlu dijadikan gestur ketuk tanpa kontrol. Dokumentasi Gestures Apple
Buat panel lengkap yang dapat dipindahkan dengan tombol anak
Buat app macOS SwiftUI di Xcode, tetapkan target deployment ke macOS 14 atau versi lebih baru, lalu ganti app dan tampilan konten yang dihasilkan dengan satu file berikut. Pertahankan hanya satu titik masuk app di target. Persyaratan macOS 14 mencakup modifier coordinateSpace(_:) modern yang digunakan di sini. Dokumentasi Apple tentang ruang koordinat
Semua item dan state berada di memori. Contoh ini memindahkan seluruh panel, termasuk tombolnya, di dalam area tetap. Contoh ini tidak mengimplementasikan pemindahan item atau memindahkan jendela aplikasi.
import SwiftUI
@main
struct GesturePanelApp: App {
var body: some Scene {
WindowGroup {
PanelDemo()
}
}
}
private struct DemoItem: Identifiable {
let id: Int
let title: String
let color: Color
}
private struct ItemTarget: View {
let item: DemoItem
let isSelected: Bool
let select: () -> Void
var body: some View {
Button(action: select) {
HStack(spacing: 10) {
Circle()
.fill(item.color)
.frame(width: 18, height: 18)
Text(item.title)
Spacer()
if isSelected {
Text("Selected")
.font(.caption)
}
}
.padding(12)
.frame(width: 220)
.background(
isSelected
? Color.accentColor.opacity(0.18)
: Color.gray.opacity(0.12)
)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
private struct PanelDemo: View {
private let items = [
DemoItem(id: 1, title: "Item A", color: .blue),
DemoItem(id: 2, title: "Item B", color: .orange),
DemoItem(id: 3, title: "Item C", color: .purple)
]
@State private var selectedID: Int? = nil
@State private var committedOffset: CGSize = .zero
@GestureState private var liveTranslation: CGSize = .zero
private var pan: some Gesture {
DragGesture(
minimumDistance: 8,
coordinateSpace: .named("stage")
)
.updating($liveTranslation) { value, translation, _ in
translation = value.translation
}
.onEnded { value in
committedOffset = CGSize(
width: committedOffset.width + value.translation.width,
height: committedOffset.height + value.translation.height
)
}
}
var body: some View {
VStack(spacing: 12) {
Text("Click an item or drag the panel")
ZStack {
Color.gray.opacity(0.06)
VStack(spacing: 10) {
Text("Drag panel")
.font(.headline)
.frame(width: 244, height: 32)
.contentShape(Rectangle())
ForEach(items) { item in
ItemTarget(
item: item,
isSelected: selectedID == item.id,
select: { selectedID = item.id }
)
}
}
.padding(16)
.background(Color.gray.opacity(0.15))
.contentShape(Rectangle())
.simultaneousGesture(pan, including: .all)
.offset(
x: committedOffset.width + liveTranslation.width,
y: committedOffset.height + liveTranslation.height
)
}
.frame(width: 640, height: 420)
.coordinateSpace(.named("stage"))
Button("Reset panel") {
committedOffset = .zero
}
}
.padding(20)
}
}
Program ini telah ditinjau berdasarkan dokumentasi API yang diperoleh, tetapi belum dikompilasi atau diuji secara interaktif di sini: lingkungan ini tidak memiliki compiler Swift atau runtime UI macOS. Build program ini di Xcode dan lakukan pemeriksaan di bawah pada versi macOS yang Anda dukung.
Alasan seretan menggunakan ambang bukan nol
DragGesture menyediakan jarak gerak minimum sebelum gestur berhasil. Nilai ilustratif 8 memberi toleransi gerak pada klik biasa sebelum induk mulai menyeret. Nilai ini merupakan pilihan aplikasi, bukan ambang yang ditetapkan Apple atau kebijakan lengkap untuk membedakan klik dari seretan. Seretan dengan jarak nol menghilangkan pembedaan tersebut. Dokumentasi Apple tentang DragGesture
Alasan gerakan sementara dan gerakan yang disimpan dipisahkan
Selama pengenalan, updating(_:body:) menulis translasi saat ini ke dalam liveTranslation. Apple mendokumentasikan bahwa state gestur ini direset ketika gestur berakhir atau dibatalkan. Contoh ini menyimpan offset akhir hanya di onEnded, sehingga pembatalan membuang pratinjau yang belum disimpan alih-alih meninggalkan translasi sementara yang sudah basi. Dokumentasi Apple tentang updating
Posisi yang ditampilkan adalah offset tersimpan ditambah translasi saat ini. Apple mendefinisikan translation sebagai perpindahan total dari awal seretan, sehingga menambahkannya berulang kali ke offset tersimpan pada setiap perubahan akan menghitung gerakan secara berlebihan. Sebagai ilustrasi, offset horizontal tersimpan sebesar 40 dan translasi saat ini sebesar 12 menghasilkan offset yang ditampilkan sebesar 52; saat seretan selesai, nilai 52 disimpan satu kali. Dokumentasi Apple tentang translation
Ruang koordinat bernama dimiliki oleh area tetap yang mengelilingi panel bergerak. Dengan demikian, perhitungan gerakan merujuk pada area acuan yang eksplisit. Tombol anak tidak menyimpan salinan posisi panel masing-masing.
Alasan pemilihan tetap dikelola induk
Induk menyimpan selectedID dan memberikan kepada setiap anak status terpilih saat ini serta closure untuk memilihnya. Ini mengikuti panduan Apple untuk menyimpan state di tampilan tertinggi yang membutuhkannya, lalu meneruskan nilai atau binding ke bawah hierarki. Dokumentasi Apple tentang State
Closure tersebut menangkap item saat ini dan meminta pembaruan state induk. Swift mendukung closure yang menangkap nilai dari konteks sekitarnya; tidak perlu menyalin atau meneruskan event AppKit untuk mengimplementasikan callback tersebut. The Swift Programming Language: Closures
Pahami area hit dan mask gestur
contentShape(Rectangle()) menentukan shape untuk hit testing. Pada label tombol, modifier ini berada setelah padding dan frame agar area label persegi panjang yang dimaksud terlihat jelas. Pada induk, modifier ini menentukan shape interaksi panel. Modifier ini tidak mengatur prioritas recognizer atau mengharuskan dua gestur bekerja sama. Dokumentasi Apple tentang content shape
Mask yang diberikan ke modifier gestur menentukan gestur mana yang tetap aktif. Opsi ini mudah disalahartikan: subviews berarti gestur induk yang ditambahkan dinonaktifkan, bukan berarti gestur tersebut otomatis bekerja sama dengan turunan. Dokumentasi Apple tentang GestureMask
| Mask | Dampak yang didokumentasikan | Kesesuaian di sini |
|---|---|---|
.all |
Mengaktifkan gestur yang ditambahkan dan gestur lain pada tampilan serta subtampilannya | Titik awal untuk partisipasi bersama |
.gesture |
Mengaktifkan gestur yang ditambahkan dan menonaktifkan gestur dalam hierarki subtampilan | Bertentangan dengan upaya mempertahankan gestur anak |
.subviews |
Mengaktifkan gestur subtampilan dan menonaktifkan gestur yang ditambahkan | Menonaktifkan seretan induk |
.none |
Menonaktifkan gestur dalam hierarki subtampilan, termasuk gestur yang ditambahkan | Menonaktifkan interaksi yang dimaksud |
Jangan menganggap highPriorityGesture sebagai solusi umum untuk callback seretan induk yang tidak muncul. Tujuan yang didokumentasikan adalah memberi gestur terpasang prioritas lebih tinggi. Contoh Apple menunjukkan induk mendahului ketukan anak. Pilih opsi ini jika prioritas tersebut memang perilaku yang diinginkan. Dokumentasi Apple tentang gestur prioritas tinggi
Gunakan handel seret terpisah agar aktivasi tetap berbeda
Untuk panel yang tombolnya tidak boleh berbagi area seret, pertahankan state dan perhitungan gerakan yang sama, tetapi pasang pan hanya pada header. Hapus .simultaneousGesture(pan, including: .all) dari panel induk.
Ganti ekspresi header yang ada dengan fragmen ini di dalam program lengkap di atas. Fragmen ini menggunakan properti pan yang sudah didefinisikan; prasyarat lainnya tidak berubah.
Text("Drag panel")
.font(.headline)
.frame(width: 244, height: 32)
.contentShape(Rectangle())
.gesture(pan)
Kini header dan tombol item menempati area yang berbeda, dan tombol tidak lagi memiliki gestur seret panel yang dipasang pada leluhurnya. Interaksi yang dimulai pada item adalah interaksi tombol; untuk memindahkan panel, mulailah dari header. Desain ini berguna ketika partisipasi bersama dapat membuat hasilnya ambigu.
Pertahankan tombol native ketika anak memulai suatu tindakan. Apple menjelaskan Button sebagai tindakan beserta label, dan mengizinkan tampilan label kustom seperti item berwarna sintetis yang digunakan di sini. Menggantinya dengan penangan penunjuk buatan sendiri mengharuskan Anda membuat ulang perilaku yang sudah disediakan kontrol tersebut. Dokumentasi Apple tentang Button
Pertahankan kepemilikan event AppKit pada batas framework
Jika panel menyertakan tampilan AppKit, diagnosis hit testing-nya secara terpisah dari komposisi gestur SwiftUI. hitTest(_:) milik AppKit mengembalikan turunan terdalam yang cocok dan membantu jendela menentukan penerima event mouse-down. Titik masuknya dinyatakan dalam koordinat superview tampilan tersebut. Dokumentasi Apple tentang hit testing AppKit
Mengembalikan induk dari hit testing mengubah target awal; itu tidak sama dengan membiarkan anak dan induk mengenali gestur secara simultan. Demikian pula, mengembalikan nil dari hit test overlay menghapus overlay tersebut dari pertimbangan—hal itu tidak menjadikan overlay pengamat pasif atas seluruh interaksi anak.
AppKit mendokumentasikan bahwa implementasi default mouseDown(with:) meneruskan pesan ke responder berikutnya. Itu adalah perilaku rantai responder, bukan jaminan bahwa setiap leluhur menerima salinan event secara mandiri setelah kontrol menanganinya. Hubungkan aktivasi kontrol dan gerakan panel melalui callback eksplisit atau recognizer yang Anda kelola. Dokumentasi Apple tentang mouse-down
Saat Anda mengelola dua recognizer gestur AppKit dan memerlukan pengenalan simultan, AppKit menyediakan gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:). Apple mendokumentasikan bahwa mengembalikan true mengizinkan pasangan tersebut mengenali gestur secara bersamaan; mengembalikan false tidak menjamin keduanya saling mengecualikan jika delegate recognizer lain mengizinkannya. Batasi kebijakan tersebut pada pasangan recognizer yang dimaksud. Dokumentasi Apple tentang delegate recognizer
Verifikasi interaksi dengan matriks perilaku sederhana
Uji gaya tombol, hierarki, dan rilis macOS persis seperti yang didukung app Anda. Pemeriksaan berikut memisahkan masalah pengenalan dari aritmetika gerakan dan memperjelas apakah permukaan bersama sesuai dengan kontrak yang Anda inginkan.
| Tindakan | Hal yang perlu diperiksa |
|---|---|
| Klik setiap item tanpa menyeret | Status terpilihnya berubah; panel tetap diam |
| Seret header melewati ambang | Panel mengikuti penunjuk dan mempertahankan posisi akhirnya |
| Seret dari item pada versi bersama | Gerakan induk berfungsi; tentukan apakah aktivasi tombol juga terjadi dan pastikan hal itu dapat diterima |
| Lakukan dua seretan berturut-turut | Seretan kedua dimulai dari posisi yang disimpan oleh seretan pertama, tanpa mengakumulasi setiap translasi perantara |
| Hentikan seretan yang sedang berlangsung | Translasi sementara dihapus; gerakan yang belum selesai tidak disimpan |
| Coba variasi handel | Item aktif secara mandiri; gerakan dimulai dari header |
| Aktifkan tombol item melalui navigasi keyboard | Pemilihan berfungsi tanpa memerlukan seretan penunjuk |
Jika area kosong panel dapat diseret dengan benar tetapi area item tidak, periksa kontrol anak dan komposisi gestur. Jika klik tidak berfungsi, periksa mask gestur, prioritas, dan area hit. Jika panel bergerak semakin cepat atau meloncat, periksa akumulasi translasi dan ruang koordinat yang dipilih sebelum menambahkan recognizer lain.
Contoh ini sengaja tidak membatasi gerakan, sehingga panel dapat bergerak melewati area yang terlihat; tombol Reset panel memulihkan posisinya. Untuk ruang kerja berbatas yang siap digunakan, tambahkan batas posisi eksplisit berdasarkan dimensi area dan panel. Pertahankan struktur kepemilikan yang sama: anak meminta tindakan, induk memiliki gerakan, dan kontrak interaksi menentukan apakah area pengenalan mereka saling tumpang tindih.
Sumber
- simultaneousGesture(_:including:) — Apple
- Gestures — Apple
- coordinateSpace(_:) — Apple
- DragGesture — Apple
- updating(_:body:) — Apple
- translation — Apple
- State — Apple
- Closures — The Swift Programming Language, Swift project
- contentShape(_:eoFill:) — Apple
- GestureMask — Apple
- highPriorityGesture(_:including:) — Apple
- Button — Apple
- hitTest(_:) — Apple
- mouseDown(with:) — Apple
- gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) — Apple