Biarkan posisi bentuk yang tersimpan tetap sama saat SwiftUI menampilkan pratinjau drag, lalu simpan translasi akhir di onEnded. Ini mempertahankan posisi awal hingga selesai tanpa bergantung pada waktu mouse-up AppKit. Berikut contoh lengkap untuk memindahkan persegi panjang di aplikasi Mac Anda sendiri.

Batas penyelesaian mana yang seharusnya memiliki status awal?

Untuk interaksi pemindahan bentuk, tugas konkretnya adalah menghitung setiap pratinjau dan posisi akhir dari posisi awal yang sama. Batas penyelesaian yang tepat adalah penyelesaian sukses gestur bentuk tersebut. Apple mendokumentasikan bahwa onEnded(_:) menyediakan nilai akhir gestur dan hanya berjalan ketika gestur berakhir dengan sukses. Apple merekomendasikan @GestureState untuk status sementara yang harus direset terlepas dari bagaimana gestur berakhir. Dokumentasi onEnded(_:) Apple.

Berikan interaksi ini dua jenis status: posisi model yang tersimpan dan translasi pratinjau sementara. Selama drag, tampilkan jumlah keduanya. Setelah berhasil selesai, terapkan translasi akhir satu kali ke posisi yang tersimpan. Ini adalah rancangan yang diturunkan dari pembedaan Apple antara pembaruan gestur sementara dan pembaruan status permanen. Menambahkan interaktivitas dengan gestur.

Informasi Pemilik dalam contoh ini Masa berlaku
Pusat persegi panjang yang tersimpan Model privat @State Bertahan setelah gestur selesai
Translasi pratinjau saat ini @GestureState Direset saat gestur menjadi tidak aktif
Translasi akhir Argumen callback onEnded Digunakan secara sinkron untuk menyimpan perpindahan
Peristiwa mouse-up AppKit Penanganan peristiwa AppKit Melaporkan pelepasan tombol; tidak menentukan penyelesaian gestur SwiftUI

Apple mendokumentasikan @State sebagai penyimpanan yang dikelola SwiftUI dan sumber kebenaran untuk nilai dalam hierarki tampilan. @GestureState otomatis direset saat gesturnya menjadi tidak aktif. Menyimpan posisi dasar di jenis pertama dan pratinjau di jenis kedua memberikan masa berlaku yang dibutuhkan setiap nilai dalam interaksi ini. State, GestureState.

Buat contoh lengkap drag persegi panjang di Mac

Buat aplikasi macOS SwiftUI di Xcode dengan target deployment macOS 14 atau yang lebih baru. Ganti deklarasi aplikasi dan tampilan konten yang dihasilkan dengan satu file berikut, dan pastikan target hanya memiliki satu deklarasi @main. Contoh ini menggunakan satu persegi panjang sintetis yang seluruhnya disimpan di memori. API ruang koordinat bernama didokumentasikan tersedia mulai macOS 14. Inisialisator drag gesture, Ruang koordinat bernama.

import SwiftUI

@main
struct GestureStartDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ShapeCanvas()
        }
    }
}

private struct SampleItem {
    var center = CGPoint(x: 160, y: 140)
}

private struct ShapeCanvas: View {
    @State private var item = SampleItem()
    @GestureState private var previewTranslation = CGSize.zero

    private let canvasWidth: CGFloat = 600
    private let canvasHeight: CGFloat = 360
    private let shapeWidth: CGFloat = 80
    private let shapeHeight: CGFloat = 60

    private func projectedCenter(_ translation: CGSize) -> CGPoint {
        let proposedX = item.center.x + translation.width
        let proposedY = item.center.y + translation.height
        let halfWidth = shapeWidth / 2
        let halfHeight = shapeHeight / 2

        return CGPoint(
            x: min(max(proposedX, halfWidth),
                   canvasWidth - halfWidth),
            y: min(max(proposedY, halfHeight),
                   canvasHeight - halfHeight)
        )
    }

    private var moveGesture: some Gesture {
        DragGesture(
            minimumDistance: 3,
            coordinateSpace: .named("shapeCanvas")
        )
        .updating($previewTranslation) { value, translation, _ in
            translation = value.translation
        }
        .onEnded { value in
            let finalCenter = projectedCenter(value.translation)
            item.center = finalCenter
        }
    }

    var body: some View {
        let displayedCenter = projectedCenter(previewTranslation)

        VStack(alignment: .leading, spacing: 12) {
            Text("Drag the blue rectangle")
                .font(.headline)

            ZStack {
                Color.gray.opacity(0.12)

                RoundedRectangle(cornerRadius: 10)
                    .fill(Color.blue)
                    .frame(width: shapeWidth, height: shapeHeight)
                    .contentShape(Rectangle())
                    .gesture(moveGesture)
                    .position(
                        x: displayedCenter.x,
                        y: displayedCenter.y
                    )
            }
            .frame(width: canvasWidth, height: canvasHeight)
            .coordinateSpace(.named("shapeCanvas"))
            .clipped()

            Text(
                "Committed center: " +
                "\(Int(item.center.x)), \(Int(item.center.y))"
            )
            .monospacedDigit()
        }
        .padding(20)
    }
}

Invarian utamanya adalah item.center tidak pernah berubah di updating. Nilainya tetap menjadi posisi dasar model gestur hingga onEnded menghitung finalCenter. Perhitungan akhir membaca value.translation secara langsung, sehingga properti pratinjau tidak harus mempertahankan nilai terakhirnya selama penyelesaian.

Gestur mengukur perpindahan dalam ruang koordinat bernama pada kanvas yang melingkupinya. position(x:y:) persegi panjang menempatkan pusatnya dalam ruang induk yang sama. Menyamakan ruang tersebut menghindari pencampuran koordinat lokal bentuk yang bergerak dengan koordinat kanvas. Apple mendokumentasikan pengukuran gestur dalam ruang bernama dan penempatan berdasarkan pusat. coordinateSpace(_:), position(x:y:).

Fungsi bersama projectedCenter juga menjaga persegi panjang tetap di dalam kanvas. Pratinjau dan penyimpanan menggunakan batas yang sama: pusat horizontal dari 40 hingga 560, dan pusat vertikal dari 30 hingga 330. Di tepi, perpindahan tetap didasarkan pada total pergeseran dari awal drag; persegi panjang bergerak menjauhi tepi begitu posisi yang diusulkan kembali berada di dalam batas.

Perpindahan yang lebih kecil daripada ambang pengenalan 3 poin yang dikonfigurasi tidak dianggap sebagai drag yang dikenali. Contoh ini sengaja menganggap awal sebagai dimulainya interaksi pemindahan bentuk yang dikenali. Persyaratan untuk mengambil status model pada peristiwa mouse-down fisik merupakan batas yang berbeda. Inisialisator drag gesture.

Mengapa posisi awal bertahan hingga pembaruan akhir

Apple mendefinisikan DragGesture.Value.translation sebagai total translasi dari awal gestur hingga peristiwa saat ini. Nilainya bersifat kumulatif, bukan delta sejak callback sebelumnya. Karena itu, menambahkan setiap translasi yang dilaporkan ke posisi model yang sudah diperbarui akan menghitung perpindahan secara berlebih. DragGesture.Value.

Perhatikan masukan berikut yang bersifat ilustratif, semuanya berada dalam batas kanvas:

Tahap Total translasi yang dilaporkan Pusat tersimpan Pusat yang ditampilkan
Sebelum drag (0, 0) (160, 140) (160, 140)
Pembaruan pertama (12, 5) (160, 140) (172, 145)
Pembaruan kedua (30, -10) (160, 140) (190, 130)
Berhasil selesai (45, 20) Menjadi (205, 160) Berakhir di (205, 160)

Setiap pratinjau menggunakan posisi dasar yang sama. Penyelesaian menerapkan (45, 20) terakhir satu kali. Drag sukses berikutnya dengan translasi (-15, 10) lalu dimulai dari (205, 160) dan menyimpan (190, 170). Perhitungan ini diperiksa menggunakan kalkulasi Python deterministik. Aplikasi SwiftUI ditinjau berdasarkan dokumentasi API yang diambil, tetapi tidak dikompilasi atau dijalankan di lingkungan ini.

Jangan gunakan startLocation sebagai pengganti pusat awal persegi panjang. Apple mendefinisikan startLocation sebagai lokasi peristiwa pertama drag. Pointer dapat mulai di dekat sudut persegi panjang, sehingga lokasi tersebut dan pusat model bentuk bisa berbeda. Demikian pula, predictedEndTranslation adalah prediksi berbasis kecepatan; gunakan translation akhir untuk interaksi penempatan langsung ini. DragGesture.Value.

Dalam rancangan ini, pembatalan tidak memerlukan pengembalian posisi tersimpan karena pembaruan pratinjau tidak pernah menimpanya. Apple mendokumentasikan bahwa status gestur direset ketika gestur menjadi tidak aktif, sedangkan onEnded hanya berjalan setelah gestur berhasil. Karena itu, perpindahan sementara menghilang tanpa menyimpan perpindahan yang dibatalkan. Kesimpulan ini mengikuti perilaku yang didokumentasikan; kesimpulan ini tidak bergantung pada asumsi tentang urutan antara reset dan callback penyelesaian sukses. GestureState, onEnded(_:).

Mengapa monitor mouse-up AppKit tidak seharusnya menghapus posisi dasar

Monitor peristiwa lokal AppKit menerima peristiwa sebelum distribusi ke aplikasi. Karena itu, menghapus status awal gestur bersama dari peristiwa mouse-up yang dipantau dapat menghapusnya sebelum penanganan di tahap berikutnya memproses peristiwa tersebut. Ini adalah konsekuensi dari batas distribusi yang didokumentasikan, bukan jaminan urutan callback SwiftUI yang terdokumentasi. addLocalMonitorForEvents(matching:handler:).

Demikian pula, NSResponder.mouseUp(with:) melaporkan bahwa pengguna melepaskan tombol mouse kiri. Kontraknya tidak menyatakan bahwa setiap callback gestur SwiftUI atau pembaruan tampilan telah selesai. Pisahkan tanggung jawab pengamatan peristiwa mentah dan penyelesaian gestur yang dikenali. mouseUp(with:).

API yang ditinjau di sini tidak menetapkan makna callback umum yang menyatakan bahwa SwiftUI telah selesai memproses semua hal terkait suatu peristiwa mentah. Blok pembersihan yang dijadwalkan atau timer singkat akan menambahkan asumsi penjadwalan tanpa menetapkan hubungan tersebut. Untuk tugas ini, pendekatan yang didukung adalah melakukan perhitungan model akhir di dalam onEnded, tempat nilai akhir gestur tersedia.

Jika interaksi harus diterapkan dalam tampilan AppKit khusus, biarkan tampilan tersebut mengelola urutan masukan dan status sesi, lalu integrasikan melalui NSViewRepresentable. Apple menyediakan hook pembuatan, pembaruan, koordinator, dan pembongkaran untuk integrasi tersebut. Simpan sesi tampilan yang direpresentasikan di tampilan AppKit atau koordinatornya, alih-alih membangunnya kembali dari setiap nilai representable. SwiftUI mengontrol tata letak tampilan yang direpresentasikan, jadi jangan mengubah frame atau bounds yang dikelolanya secara terpisah. NSViewRepresentable.

Kapan interaksi memerlukan snapshot eksplisit?

Posisi tersimpan dalam contoh ini sudah memadai karena tidak ada tindakan lain yang mengubahnya saat drag berlangsung. Untuk kanvas yang lebih besar, gunakan panduan ini sebelum memperkenalkan objek sesi terpisah:

Persyaratan Konsekuensi rancangan
Memindahkan satu item yang posisi tersimpannya tidak berubah selama pratinjau Gunakan pendekatan posisi dasar plus pratinjau yang ditunjukkan
Memindahkan beberapa item terpilih bersama-sama Pertahankan pengenal stabil dan posisi awalnya sebagai satu snapshot interaksi
Mengizinkan perintah lain mengedit posisi selama drag Tentukan apakah pengeditan itu diblokir, drag dibatalkan, atau diselesaikan berdasarkan revisi model
Mengambil status pada mouse-down fisik sebelum pengenalan Gunakan batas masukan AppKit yang dikelola secara eksplisit; pembaruan gestur pertama tidak setara
Mempertahankan sesi terpisah setelah penyelesaian Beri sesi itu pemilik dan kebijakan penghentian yang eksplisit, alih-alih mengandalkan status gestur sementara

Snapshot harus mempertahankan nilai. Struktur Swift memiliki semantik nilai, sedangkan penugasan instans kelas menyalin referensinya. Karena itu, array referensi ke objek item yang dapat diubah tidak dengan sendirinya membekukan posisi objek tersebut. Untuk memindahkan beberapa item, ambil pengenal dan nilai koordinat yang diperlukan untuk perhitungan akhir. Struktur dan Kelas — Bahasa Pemrograman Swift.

Jika Anda memperkenalkan sesi persisten, tentukan penyelesaian sukses, pembatalan, dan penghapusan tampilan secara eksplisit. Menghapusnya hanya di onEnded membuat pembatalan tidak tertangani karena callback itu hanya berjalan setelah sukses. Jangan gunakan ulang transaksi reset status gestur sebagai notifikasi penyelesaian universal: Apple mendokumentasikan bahwa closure reset-nya menyediakan transaksi untuk mereset status gestur. onEnded(_:), init(wrappedValue:reset:).

Untuk tugas pemindahan persegi panjang biasa, pertahankan posisi dasar dengan membiarkan model tersimpan tidak berubah selama pratinjau. Hitung posisi akhir dari nilai gestur sukses itu sendiri, simpan sekali, dan biarkan status gestur sementara direset. Dengan demikian, posisi awal memiliki pemilik dan masa berlaku yang jelas sepanjang perhitungan yang membutuhkannya.

Sumber