Pulihkan bentuk yang diredupkan, garis tepi, dan label seret dari callback akhir sesi sumber seret. Callback keluar milik tujuan hanya berarti penunjuk meninggalkan target tersebut. Untuk app Mac, gunakan API sesi sumber SwiftUI jika tersedia, atau biarkan sumber AppKit mengelola seretan.

Panduan ini membahas satu tugas: mengembalikan bentuk sintetis ke tampilan normal setelah pengguna selesai menyeretnya, termasuk saat tidak ada tujuan yang menerimanya. Contoh ini menyimpan kontennya di memori dan memperlakukan pelepasan yang diterima sebagai penyalinan, jadi pemulihan umpan balik tidak pernah mengharuskan sumber dihapus atau dipindahkan.

Mengapa umpan balik seret tetap aktif di luar target pelepasan?

Implementasi umum menetapkan flag isDragging saat seretan dimulai dan menghapusnya di performDrop. Dengan begitu, pembersihan bergantung pada peristiwa yang hanya dapat ditangani oleh tujuan yang menerima pelepasan. Jika dilepas di ruang kosong dalam jendela, tidak ada tujuan yang bertanggung jawab untuk mengatur ulang sumber.

Apple mendefinisikan DropDelegate sebagai antarmuka untuk tampilan yang dimodifikasi agar menerima pelepasan. Metode dropExited(info:) melaporkan bahwa seretan yang telah divalidasi keluar dari tampilan tersebut. Seretan dapat berlanjut ke target lain, kembali ke target pertama, atau berakhir di tempat lain. Karena itu, keluarnya seretan dari target tidak dapat memastikan bahwa sesi sumber telah berakhir. DropDelegate

Pembedaan yang sama berlaku untuk dropDestination(for:action:isTargeted:): isTargeted menjelaskan apakah penunjuk berada di dalam area target. Gunakan ini untuk garis tepi atau latar target. Biarkan callback sesi sumber mengontrol peredupan sumber atau label seret. dropDestination(for:action:isTargeted:)

Aturan desain yang berguna mengikuti batasan yang didokumentasikan tersebut: tetapkan setiap status visual pada pemilik peristiwa yang dapat mengakhirinya secara andal.

Umpan balik atau hasil Pemilik Titik pengaturan ulang atau penerapan
Bentuk sumber yang diredupkan Sumber seret Sesi sumber berakhir
Tujuan yang disorot Tujuan pelepasan Penunjuk keluar, dengan pembersihan defensif saat seretan lokal selesai
Item sintetis yang diterima Tujuan/model Pelepasan divalidasi dan berhasil
Sumber daya yang diekspor sementara Siklus transfer Transfer selesai, jika berlaku

Pendekatan mana yang didukung untuk digunakan app Mac?

Referensi Apple saat ini mencantumkan onDragSessionUpdated(_:) sebagai tersedia di macOS 26 dan versi lebih baru. API ini mengamati pembaruan pada sesi seret terdekat di hierarki tampilan turunannya. Pasang langsung di luar modifier seret sumber agar cakupannya jelas. Gunakan SDK yang benar-benar mendeklarasikan metode tersebut; ketersediaan saat runtime tidak membuat deklarasi SDK yang tidak ada menjadi bisa dikompilasi. onDragSessionUpdated(_:)

Ada perbedaan dokumentasi yang perlu ditangani secara terbatas: contoh modifier menampilkan fase berakhir dengan argumen lokasi dan operasi, sedangkan referensi fase khusus mendeklarasikan ended(DropOperation). Contoh di bawah mencocokkan .ended tanpa mengambil nilai terkait karena pemulihan visual tidak membutuhkannya. Periksa deklarasi SDK yang terpasang sebelum menambahkan perilaku khusus operasi. DragSession.Phase.ended(_:)

Jika target deployment atau SDK Anda tidak dapat menggunakan callback SwiftUI, sumber seret AppKit menyediakan batas penyelesaian yang terdokumentasi. Apple secara eksplisit menyatakan bahwa sumber menerima draggingSession(_:endedAt:operation:) saat seretannya berakhir atau dibatalkan. Sumber kustom harus memulai sesi itu sendiri. beginDraggingSession(with:event:source:)

Jika Anda hanya perlu memindahkan bentuk di dalam satu kanvas, pertimbangkan DragGesture lokal alih-alih sesi seret dan lepas sistem. @GestureState diatur ulang saat gesturnya menjadi tidak aktif, sehingga cocok untuk umpan balik sementara milik gesture tersebut. Pisahkan pilihan itu dari seretan sistem yang menyediakan konten yang dapat ditransfer ke tujuan. GestureState

Terapkan pembersihan yang dikelola sumber di SwiftUI

Buat app SwiftUI macOS menggunakan SDK yang menyediakan onDragSessionUpdated(_:), tetapkan target deployment ke macOS 26 atau versi lebih baru, lalu ganti deklarasi app dan tampilan konten yang dihasilkan dengan program lengkap ini. Program ini menyediakan satu string sintetis, shape.circle, dan satu area penerima.

Sumber menggunakan draggable(_:) untuk menyediakan konten yang dapat ditransfer. Tujuan hanya menerima satu string yang cocok dan mengembalikan false untuk masukan lainnya. Apple mendokumentasikan nilai Boolean hasil tujuan sebagai penanda apakah pelepasan berhasil. draggable(_:), dropDestination(for:action:isTargeted:)

import SwiftUI

@main
struct DragFeedbackDemoApp: App {
    var body: some Scene {
        WindowGroup {
            DragFeedbackDemo()
        }
    }
}

struct DragFeedbackDemo: View {
    private let payload = "shape.circle"

    @State private var isDragging = false
    @State private var isTargeted = false
    @State private var acceptedCopies = 0

    var body: some View {
        VStack(spacing: 24) {
            Text("Drag the circle into the receiving area")

            Circle()
                .fill(Color.blue)
                .frame(width: 72, height: 72)
                .opacity(isDragging ? 0.4 : 1)
                .draggable(payload)
                .onDragSessionUpdated { session in
                    switch session.phase {
                    case .initial, .active:
                        isDragging = true
                    case .ended, .dataTransferCompleted:
                        restoreFeedback()
                    default:
                        break
                    }
                }

            Text(isDragging ? "Dragging" : "Ready")

            RoundedRectangle(cornerRadius: 12)
                .fill(isTargeted
                      ? Color.blue.opacity(0.2)
                      : Color.gray.opacity(0.12))
                .overlay {
                    Text("Receiving area")
                }
                .frame(width: 280, height: 120)
                .dropDestination(for: String.self) { items, _ in
                    guard items.count == 1,
                          items.first == payload else {
                        return false
                    }
                    acceptedCopies += 1
                    return true
                } isTargeted: { targeted in
                    isTargeted = targeted
                }

            Text("Accepted copies: \(acceptedCopies)")
        }
        .padding(32)
        .frame(minWidth: 380, minHeight: 400)
    }

    private func restoreFeedback() {
        isDragging = false
        isTargeted = false
    }
}

Kedua flag memiliki tugas berbeda. isDragging mengubah opasitas sumber. isTargeted mengubah isian area penerima. Karena itu, masuk dan keluarnya penunjuk dari area penerima tidak dapat secara tidak sengaja memulihkan tampilan sumber saat seretan masih berlangsung.

Referensi fase menyebut initial sebagai sesi yang akan dimulai, active sebagai pergerakan, dan ended sebagai penyelesaian. Contoh ini memulihkan umpan balik pada ended; contoh ini juga membuat pengaturan ulang yang sama aman untuk diulang pada dataTransferCompleted. Pemulihan seretan yang gagal tidak pernah bergantung hanya pada penyelesaian transfer. DragSession.Phase

Apple menjelaskan dataTransferCompleted sebagai titik ketika item yang diseret telah ditransfer dan item sementara dapat dibersihkan. Contoh ini tidak memiliki sumber daya ekspor sementara. Dalam implementasi yang lebih besar, pisahkan pembersihan visual dari pembersihan sumber daya agar pemulihan opasitas tidak membuang sesuatu yang masih dibutuhkan oleh transfer. DragSession.Phase.dataTransferCompleted

Lingkaran tetap berada dalam hierarki selama sesi berlangsung. Pelepasan yang diterima menambah hitungan tujuan; pelepasan yang tidak diterima tidak mengubah konten. Dengan begitu, contoh ini berguna untuk mendiagnosis umpan balik secara terpisah dari semantik pengurutan ulang atau pemindahan. Kode ini diperiksa terhadap deklarasi yang diperoleh; kode ini tidak dikompilasi atau dijalankan karena lingkungan yang tersedia adalah Linux tanpa SwiftUI.

Bagaimana fallback AppKit sebaiknya mempertahankan kepemilikan peristiwa?

Untuk versi macOS lama yang masih didukung, ganti implementasi sumber seret dengan NSView yang dihosting melalui NSViewRepresentable. Representable ini membuat dan memperbarui tampilan native serta menjembatani callback-nya ke status SwiftUI. SwiftUI mengontrol tata letak tampilan yang direpresentasikan, jadi gunakan modifier ukuran SwiftUI alih-alih menetapkan sendiri frame tampilan tersebut. NSViewRepresentable

Terapkan fallback sebagai satu rangkaian yang dikelola sumber:

  1. Di mouseDown(with:) pada tampilan sumber native, simpan peristiwa klik mouse yang diterima tampilan tersebut.
  2. Di mouseDragged(with:), terapkan ambang seret yang Anda pilih dan cegah sesi kedua dimulai.
  3. Buat NSDraggingItem dari penulis pasteboard dalam memori yang membawa pengenal item sintetis.
  4. Sediakan gambar bentuk yang dihasilkan dan tetapkan frame seretnya dalam sistem koordinat tampilan sumber.
  5. Panggil beginDraggingSession(with:event:source:) menggunakan peristiwa klik mouse yang disimpan dan objek yang mengimplementasikan NSDraggingSource.
  6. Tampilkan umpan balik sumber dari draggingSession(_:willBeginAt:) dan pulihkan tanpa syarat dari draggingSession(_:endedAt:operation:).
  7. Hapus peristiwa masukan yang disimpan dan informasi sesi saat pelacakan selesai. Pertahankan sumber dan pemilik callback-nya hingga sesi selesai.

Detail ini mengikuti kontrak eksplisit AppKit: metode pemulai sesi menerima peristiwa klik mouse yang memulai sesi, frame item seret menggunakan sistem koordinat tampilan, dan sumber menerima pesan siklus hidup. NSDraggingItem menerima objek yang sesuai dengan NSPasteboardWriting; metode kemudahan satu gambarnya menetapkan frame dan konten seret. beginDraggingSession(with:event:source:), init(pasteboardWriter:), setDraggingFrame(_:contents:)

Untuk interaksi dalam app sendiri ini, kembalikan .copy untuk NSDraggingContext.withinApplication dan set operasi kosong untuk konteks lainnya. Ini adalah pilihan eksplisit untuk hanya menawarkan salinan lokal. AppKit menanyakan operasi yang diizinkan sumber melalui draggingSession(_:sourceOperationMaskFor:). draggingSession(_:sourceOperationMaskFor:)

Pulihkan tampilan untuk setiap hasil penyelesaian, bukan hanya penyalinan yang berhasil. Kondisikan perubahan model pada operasi yang benar-benar diterapkan app Anda. Titik dalam callback penyelesaian berada dalam koordinat layar; jangan membandingkannya langsung dengan batas lokal target SwiftUI. draggingSession(_:endedAt:operation:)

Tampilan native transparan yang ditempatkan di belakang .draggable SwiftUI yang sudah ada tidak menjadi sumber sesi tersebut hanya dengan mematuhi NSDraggingSource. Sumbernya adalah objek yang diteruskan saat sesi dimulai. Pilih satu pemilik untuk memulai dan menyelesaikan sesi, lalu batasi permukaan seret native pada area interaksi yang memang dimaksudkan untuk bentuk tersebut.

Mengapa tidak menggunakan pemantauan mouse-up atau deinisialisasi provider?

Pemantau mouse-up seluruh app mengamati pengiriman masukan, yang merupakan batas berbeda dari penyelesaian seretan. Apple mendokumentasikan bahwa pemantau peristiwa lokal tidak menerima peristiwa yang ditangani oleh loop pelacakan bersarang. Batasan tersebut membuat pemantau tidak cocok sebagai pengganti callback penyelesaian sumber yang terdokumentasi. addLocalMonitorForEvents(matching:handler:)

Demikian pula, deinit milik provider item melaporkan masa hidup objek. Swift memanggil deinisialisasi tepat sebelum sebuah instance didealokasi; kontrak tersebut tidak menetapkan kapan seretan berakhir. Menggunakan penghancuran provider sebagai sinyal penyelesaian mengaitkan status UI dengan kepemilikan referensi, bukan siklus hidup seretan. Deinitialization

Menambahkan DragGesture kedua bersama seretan sistem memiliki masalah konseptual yang sama: statusnya milik gesture tersebut. Gunakan @GestureState saat gesture lokal mengelola interaksi; gunakan callback sesi sumber seret saat seretan sistem yang mengelolanya. GestureState

Verifikasi perbaikan dengan matriks penyelesaian

Gunakan lingkaran sintetis untuk memeriksa setiap jalur secara terpisah. Ini adalah perilaku yang diharapkan dari implementasi, bukan hasil pengujian yang dilaporkan.

Interaksi Umpan balik sumber yang diharapkan Hasil tujuan yang diharapkan
Lepaskan lingkaran di dalam area penerima Kembali normal Hitungan bertambah satu
Lepaskan di ruang kosong dalam jendela Kembali normal Hitungan tidak berubah
Keluar dari area penerima saat masih menyeret Tetap redup Sorotan target dihapus
Masuk kembali dan selesaikan pelepasan yang valid Kembali normal saat penyelesaian Hitungan bertambah satu
Lepaskan di luar jendela app tanpa tujuan yang menerima Kembali normal Hitungan tidak berubah
Batalkan seretan yang aktif Kembali normal Hitungan tidak berubah
Ulangi beberapa seretan yang gagal Setiap sesi dipulihkan secara normal Tidak ada salinan yang diterima dan menumpuk

Jika sumber tetap redup, periksa terlebih dahulu apakah pengamat penyelesaiannya membungkus sumber seret yang sebenarnya dan apakah sumber masih ada. Jika hanya sorotan target yang tertinggal, periksa penanganan masuk/keluar target dan pembersihan bersama. Jika hitungan yang diterima berubah saat pelepasan ditolak, periksa validasi dan mutasi model secara terpisah dari pemulihan visual.

Perbaikan yang bertahan lama adalah memperjelas kepemilikan penyelesaian: sumber memulihkan umpan balik untuk seluruh sesi, tujuan menangani status hover dan konten yang diterimanya sendiri, dan pembersihan transfer mengikuti siklus hidupnya yang terdokumentasi. Dengan tanggung jawab yang terpisah ini, pelepasan di luar semua target tidak menyisakan status visual yang menunggu callback tujuan.

Sumber