Saat seretan kanvas berakhir, hitung geometri akhir item dalam ruang koordinat target. Uji penampungan penuh terlebih dahulu, lalu tumpang tindih dengan luas positif. Tutorial ini menerapkan aturan tersebut untuk item persegi panjang di app Mac Anda sendiri dan menjelaskan padanannya di AppKit.

Tentukan hal yang dihitung sebagai tumpang tindih dan penampungan

Bayangkan kanvas desktop sederhana dengan persegi panjang biru yang dapat dipindahkan dan target abu-abu yang tetap. Tugas pembaca adalah melaporkan apakah item yang dilepas sepenuhnya berada di dalam target, sebagian bertumpang tindih dengannya, atau tidak berbagi area sama sekali. Semua dalam contoh ini bersifat sintetis dan disimpan di memori.

Penampungan bersifat terarah: target harus menampung item. Apple mendokumentasikan bahwa CGRectContainsRect(target, item) mengembalikan true ketika gabungan persegi panjang sama dengan target. Ini mencakup item yang ukurannya persis sama dengan target. Memeriksa penampungan terlebih dahulu mencegah item yang sepenuhnya tertampung dilaporkan sebagai tumpang tindih sebagian. Dokumentasi CGRectContainsRect

Dalam contoh ini, tumpang tindih mensyaratkan adanya area bersama dengan lebar positif dan tinggi positif. Sekadar menyentuh tepi atau sudut dihitung sebagai No area overlap. Hitung irisan dan periksa dimensinya alih-alih membiarkan perilaku saat bersentuhan tidak ditentukan. Apple mendokumentasikan bahwa CGRectIntersection mengembalikan persegi panjang bersama, atau persegi panjang null jika tidak ada irisan. Dokumentasi CGRectIntersection

Hasil Aturan yang digunakan dalam tutorial ini Contoh
Contained Target menampung seluruh item, termasuk batas yang berimpit Item muat di dalam target atau ukurannya persis sama
Overlap Penampungan gagal, tetapi area bersama memiliki lebar dan tinggi positif Item melintasi tepi target
No area overlap Tidak ada irisan dengan luas positif Item terpisah atau hanya menyentuh tepi
Invalid geometry Salah satu persegi panjang memiliki koordinat non-finit atau dimensi nonpositif Item dengan lebar nol

Ini adalah aturan aplikasi yang diturunkan dari operasi geometri. Jika antarmuka Anda memerlukan jarak tepi di dalam target, tentukan jarak tersebut secara terpisah dan uji terhadap persegi panjang penerimaan yang lebih kecil. Pastikan target yang terlihat dan aturan penerimaannya mudah dipahami oleh orang yang menyeret item.

Gunakan satu ruang koordinat dan translasi akhir seretan

Persegi panjang yang Anda gambar dan persegi panjang yang Anda klasifikasikan harus menggambarkan posisi yang sama. Dalam contoh ini, item dan target sama-sama menggunakan koordinat kanvas. Kanvas diberi nama ruang koordinat, dan DragGesture milik item menggunakan nama yang sama. Modifier coordinateSpace(_:) Apple saat ini mendukung pola ini di macOS 14 dan versi lebih baru. Dokumentasi ruang koordinat bernama

Simpan persegi panjang item yang telah ditetapkan secara terpisah dari translasi seretan sementara. Selama gestur berlangsung, gambar persegi panjang yang telah ditetapkan dengan offset translasi tersebut. Setelah selesai dengan sukses, hitung lagi persegi panjang akhir dari nilai callback, klasifikasikan, lalu tetapkan satu kali.

Apple mendefinisikan translation sebagai perpindahan total dari awal seretan hingga peristiwa saat ini. Karena itu, jika seluruh translasi berulang kali ditambahkan ke posisi yang sudah diperbarui, pergerakannya akan terakumulasi berlebihan. Terapkan translasi pada posisi yang telah ditetapkan dan tidak berubah. Dokumentasi translasi seretan

Titik pelepasan saja tidak cukup untuk menentukan apakah seluruh item muat di dalam target. Seseorang dapat memegang persegi panjang dekat salah satu sudut sehingga sebagian besar bentuknya berada di luar, meskipun penunjuk berada di dalam. Menerapkan translasi juga mempertahankan offset awal saat item dipegang, tanpa memindahkan pusat item ke penunjuk.

Buat contoh persegi panjang SwiftUI yang lengkap

Buat app SwiftUI macOS di Xcode dengan target deployment macOS 14 atau versi lebih baru. Ganti titik masuk app yang dibuat secara otomatis dengan dua blok di bawah, letakkan berurutan dalam satu file Swift. Hapus titik masuk app yang asli agar target hanya memuat satu deklarasi @main. Kedua blok tersebut membentuk program lengkap.

Blok pertama mendefinisikan pengklasifikasi geometri. Fungsi ini secara eksplisit menolak dimensi yang tidak valid alih-alih diam-diam mengubahnya menjadi persegi panjang lain. Enumerasi Swift membedakan empat kemungkinan hasil; nilai raw string-nya digunakan untuk menampilkan pesan. Dokumentasi enumerasi Swift

import SwiftUI
import CoreGraphics

enum Placement: String {
    case contained = "Contained"
    case overlap = "Overlap"
    case separate = "No area overlap"
    case invalid = "Invalid geometry"
}

func isUsable(_ rect: CGRect) -> Bool {
    let values = [rect.origin.x, rect.origin.y,
                  rect.width, rect.height,
                  rect.maxX, rect.maxY]
    return values.allSatisfy { $0.isFinite }
        && rect.width > 0 && rect.height > 0
}

func classify(item: CGRect, target: CGRect) -> Placement {
    guard isUsable(item), isUsable(target) else {
        return .invalid
    }
    if CGRectContainsRect(target, item) {
        return .contained
    }
    let shared = CGRectIntersection(item, target)
    if !shared.isNull && shared.width > 0 && shared.height > 0 {
        return .overlap
    }
    return .separate
}

Blok kedua menyediakan app dan kanvas. Pasang gestur pada persegi panjang yang dapat dipindahkan, seperti yang dijelaskan dalam dokumentasi DragGesture Apple. Ukuran kanvas yang tetap membuat geometri contoh ini jelas. Dokumentasi DragGesture

@main
struct RectanglePlacementApp: App {
    var body: some Scene {
        WindowGroup {
            PlacementCanvas()
        }
    }
}

struct PlacementCanvas: View {
    private let target = CGRect(x: 200, y: 100,
                                width: 200, height: 160)
    @State private var item = CGRect(x: 40, y: 120,
                                     width: 80, height: 60)
    @State private var message = "Drag the blue item, then release."
    @GestureState private var translation: CGSize = .zero

    private var preview: CGRect {
        item.offsetBy(dx: translation.width,
                      dy: translation.height)
    }

    private var move: some Gesture {
        DragGesture(minimumDistance: 1,
                    coordinateSpace: .named("placementCanvas"))
            .updating($translation) { value, state, _ in
                state = value.translation
            }
            .onEnded { value in
                let finalItem = item.offsetBy(
                    dx: value.translation.width,
                    dy: value.translation.height
                )
                let result = classify(item: finalItem, target: target)
                message = result.rawValue
                if result != .invalid {
                    item = finalItem
                }
            }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text(message)
            ZStack(alignment: .topLeading) {
                Color.white
                Rectangle()
                    .fill(Color.gray.opacity(0.25))
                    .frame(width: target.width, height: target.height)
                    .position(x: target.midX, y: target.midY)
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: item.width, height: item.height)
                    .position(x: preview.midX, y: preview.midY)
                    .gesture(move)
            }
            .frame(width: 640, height: 380)
            .coordinateSpace(.named("placementCanvas"))
            Button("Reset") {
                item = CGRect(x: 40, y: 120, width: 80, height: 60)
                message = "Drag the blue item, then release."
            }
        }
        .padding()
    }
}

Seret item biru ke atas target abu-abu, lalu lepaskan. Pesan mencatat penempatan terakhir yang diselesaikan. Tumpang tindih sebagian dan penempatan terpisah tetap berada di lokasi saat dilepas; penampungan tidak otomatis mengubah keanggotaan induk atau menempelkan item ke kontainer lain. Tombol Reset mengembalikan persegi panjang ke posisi awal.

Dalam gestur ini, pembaruan posisi persisten sengaja tidak dilakukan pada callback pembaruan. @GestureState hanya menyimpan translasi pratinjau dan direset ketika gestur tidak aktif. Apple menetapkan bahwa onEnded hanya dijalankan setelah gestur berhasil diselesaikan, dan menyarankan state gestur untuk nilai yang harus direset terlepas dari bagaimana gestur berakhir. Akibatnya, gestur yang terputus dapat membuang pratinjau sambil mempertahankan persegi panjang yang telah ditetapkan dan hasil sebelumnya. Dokumentasi GestureState, Dokumentasi onEnded

Implementasi ini menganggap persegi panjang terisi sebagai geometrinya. Menambahkan bayangan dekoratif atau garis tepi tidak memperluas persegi panjang penerimaan. Jika Anda menambahkan pengubahan ukuran, zoom, atau rotasi, perbarui model geometri dan konversi koordinat sebelum mengandalkan pengklasifikasi yang sama.

Periksa kasus batas sebelum menambahkan lebih banyak item

Gunakan kasus sintetis berikut dengan target pada contoh: titik asal (200, 100), lebar 200, tinggi 160. Koordinat menggambarkan persegi panjang item dalam ruang kanvas yang sama.

Titik asal item Ukuran item Hasil yang diharapkan Alasan
(220, 120) 80 × 60 Contained Setiap tepi berada di dalam target
(360, 120) 80 × 60 Overlap Item melewati tepi kanan target
(400, 120) 80 × 60 No area overlap Hanya tepi vertikal yang bersentuhan
(401, 120) 80 × 60 No area overlap Ada celah antara persegi panjang
(200, 100) 200 × 160 Contained Persegi panjang yang sama ukurannya memenuhi syarat penampungan
(180, 80) 240 × 200 Overlap Item mencakup target, sehingga arah penampungan terbalik

Perhitungan dalam tabel diperiksa dengan kalkulasi deterministik independen. Program Swift telah ditinjau, tetapi tidak dikompilasi di lingkungan riset, yang tidak menyediakan compiler Swift. Bangun program ini di target app macOS dan coba posisi pelepasan di dekat setiap batas.

Untuk beberapa item dalam memori, pertahankan identitas item yang dipilih saat seretan dimulai. Saat selesai, bandingkan geometri akhirnya dengan target yang memenuhi syarat dan kecualikan item itu sendiri. Jika beberapa target memenuhi syarat, tentukan aturan pemilihan yang konsisten, misalnya urutan target yang ditetapkan secara eksplisit. Klasifikasi geometri saja tidak menentukan target mana yang seharusnya memiliki item tersebut.

Terapkan aturan geometri yang sama pada kanvas AppKit

Untuk tampilan AppKit kustom yang sudah ada, pisahkan pengklasifikasi dari penerimaan peristiwa. mouseUp(with:) melaporkan pelepasan tombol tetikus kiri; gunakan itu untuk menyelesaikan hanya seretan yang memang dimulai oleh kanvas Anda. Pelepasan tetikus yang tidak terkait tidak boleh memicu penempatan. Dokumentasi pelepasan tombol tetikus AppKit

Urutan peristiwa yang praktis:

  1. Di mouseDown(with:), pilih item sintetis menggunakan aturan hit-testing yang sudah digunakan kanvas. Simpan identitasnya, persegi panjang awal, dan posisi penunjuk saat mulai.
  2. Konversikan setiap lokasi peristiwa tetikus yang relevan ke koordinat kanvas. Untuk tampilan yang terpasang pada jendelanya, ekspresi konversinya adalah convert(event.locationInWindow, from: nil).
  3. Di mouseDragged(with:), turunkan pratinjau dari persegi panjang yang tersimpan dan perpindahan penunjuk.
  4. Di mouseUp(with:), hitung ulang perpindahan menggunakan peristiwa pelepasan, klasifikasikan persegi panjang akhir, lalu tetapkan satu kali dan hapus seretan aktif.

Apple mendokumentasikan locationInWindow sebagai lokasi peristiwa tetikus dalam koordinat jendela. Overload konversi titik dari NSView.convert(_:from:) mengonversi dari koordinat jendela ketika argumen tampilannya adalah nil. Ini mencegah perbandingan titik jendela dengan persegi panjang yang dinyatakan dalam koordinat lokal kanvas. Dokumentasi lokasi peristiwa, Dokumentasi konversi titik

Pertahankan penanganan peristiwa pada komponen yang sudah mengelola interaksi tersebut. Apple membedakan subclass NSView langsung yang menangani suatu peristiwa dari subclass yang harus membiarkan induknya menangani peristiwa yang tidak dikonsumsi. Ikuti perilaku responder yang didokumentasikan itu; menambahkan logika penempatan tidak membenarkan intersepsi rangkaian peristiwa tampilan lain atau pemrosesan pelepasan yang sama melalui dua handler independen. Panduan penanganan peristiwa NSView

Ketahui kapan persegi panjang tidak dapat menjawab pertanyaan tentang bentuk

Contoh ini memberikan jawaban yang tepat untuk persegi panjang terisi yang sejajar dengan sumbu. Untuk lingkaran, garis luar cekung, lubang, atau bentuk yang diputar, persegi panjang pembatas hanya memberikan uji geometri awal. Area bersama persegi panjang tersebut dapat mencakup ruang yang tidak terisi oleh salah satu bentuk yang terlihat.

Uji apakah penunjuk berada di dalam jalur juga tidak dapat memastikan bahwa bentuk lain sepenuhnya tertampung. NSBezierPath.contains(_:) dari AppKit menerima satu titik dalam sistem koordinat jalur, dan Apple menyatakan bahwa metode ini tidak memperhitungkan lebar garis. Karena itu, menguji pusat item hanya menjawab apakah pusat tersebut berada di dalam jalur. Dokumentasi penampungan titik NSBezierPath

Untuk jalur terisi di macOS 14 dan versi lebih baru, SwiftUI mendokumentasikan operasi boolean yang didukung: Path.intersection(_:eoFill:) menghasilkan area terisi bersama, dan Path.subtracting(_:eoFill:) menghasilkan bagian area terisi pertama yang berada di luar area kedua. Operasi ini menyediakan pendekatan berbasis jalur untuk menyelidiki tumpang tindih dan bagian item yang berada di luar target. Selaraskan kedua jalur dalam ruang koordinat yang sama dan gunakan aturan isian yang sesuai dengan cara Anda merendernya. Dokumentasi irisan Path, Dokumentasi pengurangan Path

Operasi tersebut berlaku untuk area terisi dan mengasumsikan subjalur yang tidak tertutup sebagai jalur tertutup. Garis yang hanya digambar dengan stroke atau aturan aplikasi mengenai kontak batas memerlukan definisi geometri tersendiri. Gunakan pengklasifikasi persegi panjang untuk item persegi panjang; perluas lapisan geometri secara terencana saat kanvas mulai menggambar bentuk lain.

Hasil pelepasan yang andal berasal dari tiga pilihan konkret: pertahankan geometri awal seretan, hitung item akhir dalam ruang koordinat target, dan terapkan kebijakan penampungan serta tumpang tindih yang eksplisit. Dengan pilihan tersebut ditetapkan, antarmuka dapat melaporkan hasil secara konsisten tanpa mengaitkan geometri dengan perutean peristiwa atau kepemilikan item.

Sumber