Untuk memilih bentuk pada kanvas SwiftUI Mac, baca kedudukan penuding dalam ruang koordinat paparan kanvas, kemudian songsangkan transformasi yang digunakan untuk melukis. Dengan zum dan sorot, tolak ofset sorot dan bahagikan dengan zum sebelum menguji bentuk anda.

Tutorial ini membina kanvas desktop kecil yang mengandungi dua segi empat tepat sintetik. Anda akan menyerlahkan segi empat tepat di bawah penuding dan memilih segi empat tepat apabila gerak isyarat penuding berakhir, termasuk selepas menukar zum atau sorot. Semua data lukisan kekal dalam memori.

Ruang koordinat manakah yang patut menerima peristiwa penuding?

Pastikan tiga ruang berbeza: sistem koordinat yang menyimpan bentuk anda, sistem koordinat setempat bagi paparan kanvas, dan sebarang koordinat tetingkap yang dibekalkan oleh AppKit. Sesuatu titik hanya berguna apabila anda tahu ruang koordinat yang diwakilinya.

onContinuousHover(coordinateSpace:perform:) SwiftUI membekalkan lokasi penuding dalam ruang koordinat yang diminta; nilai lalainya ialah .local. Ia menghantar .active(location) semasa penuding berada di dalam paparan dan .ended apabila penuding keluar. Lampirkannya terus pada kanvas supaya ruang setempatnya sepadan dengan port pandangan lukisan. Dokumentasi tuding berterusan Apple.

Ruang koordinat bernama berguna apabila paparan induk sengaja memiliki kawasan interaksi. Kedua-dua pengendali tuding dan gerak isyarat perlu meminta ruang bernama yang sama. Apple mentakrifkan .local, .global, dan .named(_:) sebagai ruang koordinat bentangan; menamakan ruang tidak mentakrifkan model lukisan aplikasi. Dokumentasi ruang koordinat Apple.

Perbezaan itu penting kerana Canvas melakukan lukisan mod langsung. Apple menyatakan dengan jelas bahawa elemen lukisan individu, termasuk simbol, tidak menerima interaktiviti sendiri. Pendekatan yang disokong di sini ialah mengendalikan interaksi pada kanvas yang mengandungi elemen tersebut dan melakukan ujian sentuhan model sendiri. Dokumentasi Canvas Apple.

Bagaimanakah anda menyongsangkan transformasi lukisan?

Pilih konvensyen lukisan yang jelas. Dalam contoh ini, koordinat model meningkat ke kanan dan ke bawah. Zum menskalakan unit model, manakala sorot ialah ofset yang diukur dalam titik setempat kanvas. Pemetaan ke hadapan dan songsangannya ialah:

viewX = modelX * zoom + panX
viewY = modelY * zoom + panY

modelX = (viewX - panX) / zoom
modelY = (viewY - panY) / zoom

Persamaan ini terhasil daripada pemetaan yang dipilih itu. Untuk melukisnya, alihkan konteks grafik mengikut sorot, kemudian skalakannya mengikut zum. Apple mendokumenkan bahawa kaedah ini mengemas kini transformasi konteks dan mempengaruhi operasi lukisan seterusnya. Menukar tertibnya akan mengubah pemetaan, jadi pastikan songsangan sepadan dengan urutan lukisan yang tepat. Dokumentasi terjemahan, dokumentasi penskalaan.

Untuk semakan ilustrasi, gunakan zum 2, sorot (40, 30), dan kedudukan penuding setempat (180, 110). Menolak sorot menghasilkan (140, 80); membahagikannya dengan zum menghasilkan kedudukan model (70, 40). Melukis titik model itu mengembalikannya ke (180, 110). Oleh itu, segi empat tepat yang bingkai modelnya mengandungi (70, 40) sepatutnya dikesan.

Kekalkan pengiraan ini dalam satu jenis kongsi. Pemapar dan kod interaksi harus menggunakan pemetaan yang sama, bukannya mengekalkan formula ofset yang berasingan. Contoh ini hanya menerima nilai zum positif yang terhingga; sifar tidak boleh disongsangkan.

Bina contoh lengkap untuk tuding dan pemilihan

Gunakan projek aplikasi SwiftUI macOS dengan sasaran penggunaan macOS 14 atau lebih baharu. Sasaran itu menyokong lebihan beban CoordinateSpaceProtocol yang digunakan oleh pengubah tuding. Tambahkan fail model berikut, kemudian gantikan pengisytiharan aplikasi yang dijana projek dengan fail kedua. Pastikan hanya ada satu pengisytiharan @main dalam sasaran. Ketersediaan dan tandatangan API tuding.

Takrifkan geometri model dan pemetaannya

Cipta CanvasGeometry.swift dengan kandungan lengkap berikut. Bingkai segi empat tepat kekal dalam koordinat model sepanjang interaksi.

import SwiftUI

struct CanvasMapping {
    let zoom: CGFloat
    let pan: CGSize

    init(zoom: CGFloat, pan: CGSize) {
        precondition(zoom.isFinite && zoom > 0)
        precondition(pan.width.isFinite && pan.height.isFinite)
        self.zoom = zoom
        self.pan = pan
    }

    func modelPoint(from viewPoint: CGPoint) -> CGPoint {
        CGPoint(
            x: (viewPoint.x - pan.width) / zoom,
            y: (viewPoint.y - pan.height) / zoom
        )
    }

    func apply(to context: inout GraphicsContext) {
        context.translateBy(x: pan.width, y: pan.height)
        context.scaleBy(x: zoom, y: zoom)
    }
}

struct CanvasItem {
    let id: Int
    let frame: CGRect
}

Geometri dan pemetaan ialah struktur Swift. Semantik nilainya memudahkan anda menghantar nilai semasa kepada kod lukisan dan ujian sentuhan tanpa memperkenalkan objek boleh ubah kongsi yang lain. Panduan bahasa Swift mendokumenkan struktur sebagai jenis nilai. Struktur dan Kelas.

Lukis dan berinteraksi dengan port pandangan yang sama

Cipta PointerCanvasApp.swift, lalu gantikan pengisytiharan aplikasi yang dijana. Fail lengkap ini menggunakan jenis daripada CanvasGeometry.swift.

import SwiftUI

@main
struct PointerCanvasApp: App {
    var body: some Scene {
        WindowGroup {
            PointerCanvasView()
        }
    }
}

struct PointerCanvasView: View {
    private let items = [
        CanvasItem(id: 1, frame: CGRect(
            x: 40, y: 30, width: 120, height: 80)),
        CanvasItem(id: 2, frame: CGRect(
            x: 120, y: 70, width: 120, height: 80))
    ]

    @State private var zoom: CGFloat = 2
    @State private var pan = CGSize(width: 40, height: 30)
    @State private var pointerInView: CGPoint?
    @State private var selectedID: Int?

    private var mapping: CanvasMapping {
        CanvasMapping(zoom: zoom, pan: pan)
    }

    private func hit(at viewPoint: CGPoint) -> Int? {
        let point = mapping.modelPoint(from: viewPoint)
        return items.reversed().first {
            $0.frame.contains(point)
        }?.id
    }

    var body: some View {
        let currentMapping = mapping
        let hoveredID = pointerInView.flatMap { hit(at: $0) }

        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Button("Zoom out") {
                    zoom = max(0.5, zoom / 2)
                }
                Button("Zoom in") {
                    zoom = min(4, zoom * 2)
                }
                Button("Pan right") {
                    pan.width += 20
                }
                Button("Reset view") {
                    zoom = 2
                    pan = CGSize(width: 40, height: 30)
                }
                Text("Selected: \(selectedID.map { String($0) } ?? "none")")
            }

            Canvas { context, _ in
                var drawing = context
                currentMapping.apply(to: &drawing)

                for item in items {
                    let path = Path(item.frame)
                    drawing.fill(
                        path,
                        with: .color(
                            item.id == hoveredID ? .orange : .blue
                        )
                    )
                    if item.id == selectedID {
                        drawing.stroke(
                            path,
                            with: .color(.black),
                            lineWidth: 2 / currentMapping.zoom
                        )
                    }
                }
            }
            .background(Color.white)
            .contentShape(Rectangle())
            .onContinuousHover(coordinateSpace: .local) { phase in
                switch phase {
                case .active(let point):
                    pointerInView = point
                case .ended:
                    pointerInView = nil
                }
            }
            .gesture(
                DragGesture(minimumDistance: 0, coordinateSpace: .local)
                    .onEnded { value in
                        selectedID = hit(at: value.location)
                    }
            )
            .clipped()
        }
        .padding(16)
        .frame(minWidth: 640, minHeight: 420)
    }
}

Kanvas menggunakan kawasan interaksi segi empat tepat, termasuk ruang kosong antara bentuk. contentShape(Rectangle()) mentakrifkan bentuk ujian sentuhan paparan; ujian frame.contains yang berasingan menentukan item sintetik yang berada di bawah penuding. Dokumentasi bentuk kandungan Apple.

Pemilihan menggunakan DragGesture dengan jarak minimum sifar dan membaca location bagi peristiwa penamat. Tingkah laku yang ditetapkan ialah memilih segi empat tepat di bawah kedudukan pelepasan, walaupun penuding bergerak semasa ditekan. Ini ialah pemilih berdasarkan kedudukan pelepasan, bukannya pengecam klik yang ketat. Apple mendokumenkan ambang jarak gerak isyarat dan konfigurasi ruang koordinat, serta mengenal pasti location sebagai kedudukan peristiwa semasa. DragGesture, DragGesture.Value.

Item dilukis mengikut urutan tatasusunan dan diuji dalam urutan terbalik. Oleh itu, segi empat tepat 2 diutamakan pada kawasan tindihan, selaras dengan susunan tindanan visual contoh ini. Klik pada ruang kosong sengaja mengosongkan pilihan. Bingkai bentuk dan kandungan tatasusunan tidak pernah berubah.

Paparan memiliki keadaan penuding dan pemilihan, manakala pemapar membacanya. Apple mengesyorkan agar keadaan peribadi diisytiharkan dalam paparan tertinggi yang memerlukannya dan agar binding dihantar apabila paparan turunan memerlukan akses tulis. Jika kemudian paparan induk memiliki koleksi item atau pemilihan, pindahkan pemilikan ke sana dan hantar nilai atau binding yang diperlukan kepada paparan ini. Dokumentasi keadaan Apple.

Bagaimanakah anda boleh menyemak pemetaan sebelum menambah gerak isyarat lain?

Mulakan dengan titik model yang diketahui dan kira kedudukan setempat yang dijangkakan. Titik semakan ini menggunakan zum dan sorot awal contoh:

Titik model Titik setempat kanvas yang dijangkakan Hasil yang dijangkakan
(70, 40) (180, 110) Segi empat tepat 1
(140, 90) (320, 210) Segi empat tepat 2, di kawasan tindihan
(10, 10) (60, 50) Tiada segi empat tepat

Kedudukan setempat diukur dari kanvas itu sendiri; bar alat dan ruang tepi luar berada di luar ruang tersebut. Gerakkan penuding merentasi kedua-dua segi empat tepat, lepaskannya pada kawasan tindihan, kemudian cuba Zum keluar dan Sorot ke kanan. Serlahan harus mengikuti geometri yang kelihatan pada setiap tetapan.

Pengiraan songsangan ilustrasi dan 12 semakan pergi balik hadapan/songsang dilaksanakan menggunakan aritmetik Python. Pengkompil Swift dan masa jalan UI macOS tidak tersedia dalam persekitaran penyelidikan, jadi aplikasi Swift diperiksa tetapi tidak dikompil atau diuji secara interaktif.

Jika semakan gagal, gunakan gejalanya untuk mengenal pasti puncanya:

  • Ofset malar menunjukkan pengendali input mungkin tergolong dalam paparan lain atau sorot telah ditinggalkan.
  • Ralat yang bertambah mengikut jarak menunjukkan penukaran skala tiada atau tertib transformasi tidak sepadan.
  • Hasil yang dicerminkan secara menegak menunjukkan terdapat pembalikan paksi y tambahan atau titik AppKit yang belum ditukar.
  • Pemilihan yang salah hanya di kawasan tindihan menunjukkan tertib lukisan dan ujian sentuhan tidak konsisten.

Simpan titik tuding setempat mentah, seperti yang dilakukan dalam contoh, dan terbitkan titik model daripadanya menggunakan pemetaan semasa. Ini mengelakkan penyimpanan kedudukan tuding dalam ruang model yang dikira menggunakan zum terdahulu. Pemilihan menggunakan lokasi peristiwa gerak isyarat itu sendiri, jadi ia tidak bergantung pada panggil balik tuding terdahulu.

Apakah yang berubah apabila peristiwa datang daripada AppKit?

Jika paparan AppKit tersuai sedia ada mengendalikan interaksi tetikus, tukarkan peristiwa pada sempadan itu sebelum menggunakan songsangan lukisan. NSEvent.locationInWindow biasanya menggunakan koordinat asas tetingkap yang berkaitan. Apple menyatakan bahawa peristiwa dengan tetingkap nil sebaliknya boleh mengandungi koordinat skrin, jadi pastikan dahulu bahawa peristiwa itu milik tetingkap paparan penerima. Dokumentasi lokasi peristiwa Apple.

Dalam NSView penerima itu, ungkapan convert(event.locationInWindow, from: nil) menukar titik tetingkap kepada koordinat setempat paparan. Apple mendokumenkan nil sebagai sumber koordinat tetingkap untuk penukaran ini. Hanya selepas itu gunakan songsangan transformasi yang dipakai oleh pemapar paparan tersebut. Dokumentasi penukaran titik Apple.

Semak orientasi pada sempadan paparan. Paparan AppKit biasa mempunyai asalan di kiri bawah dengan nilai y meningkat ke atas; paparan yang isFlippednya memulangkan true mempunyai asalan di kiri atas dengan nilai y meningkat ke bawah. Permukaan lukisan AppKit tersuai boleh menggunakan konvensyen kedua untuk dipadankan dengan model ini. Biarkan penukaran paparan mengendalikan perbezaan antara sistem koordinat paparan, dan elakkan pembalikan tambahan selepas itu. Dokumentasi koordinat terbalik Apple.

Apabila mendedahkan paparan sedemikian melalui NSViewRepresentable, kekalkan pengendalian peristiwa AppKit dalam paparan yang diwakilkan dan sampaikan hasil melalui panggil balik atau koordinator yang jelas. Segarkan sambungan itu semasa kemas kini dan bersihkan sumber yang dimiliki semasa proses penguraian. SwiftUI mengawal bentangan paparan yang diwakilkan, jadi jangan tetapkan frame atau boundsnya untuk membetulkan ralat pemetaan. Dokumentasi kitar hayat representable Apple.

Bagi contoh SwiftUI tulen, kekalkan interaksi yang dilampirkan pada kanvas. Jika permukaan AppKit menggantikannya, berikan permukaan itu pemilikan kawasan interaksi. Penukaran koordinat harus menterjemahkan peristiwa yang dibekalkan oleh pemilik sedia ada; ia tidak sepatutnya memerlukan salinan kedua dihantar kepada pengendali pemilihan lain yang bersaing.

Bagaimanakah pemetaan patut berkembang untuk kanvas yang lebih besar?

Contoh ini sengaja menyokong zum positif seragam dan terjemahan. Jika anda menambah putaran, penskalaan tidak seragam atau asalan lukisan yang berbeza, gantikan songsangan ringkas dengan songsangan transformasi lukisan penuh. Tolak pemetaan yang tidak boleh disongsangkan sebelum interaksi dan kekalkan semakan pergi balik menggunakan titik yang diketahui.

Untuk menyeret item pada masa hadapan, rekodkan item dan ofset cengkaman dalam ruang model pada permulaan gerak isyarat. Kemas kini item yang sama sepanjang gerak isyarat itu; tetapkan dengan jelas sama ada pemetaan port pandangan kekal tetap atau berubah semasa seretan. Mengulangi ujian sentuhan untuk memilih item baharu pada setiap pergerakan akan mengubah pemilikan interaksi apabila bentuk bertindih.

Gunakan satu ruang koordinat port pandangan, satu pemetaan lukisan dan satu songsangan yang sepadan. Setelah semuanya sepadan, tuding dan pemilihan boleh beroperasi pada geometri model yang stabil sementara zum dan sorot mengubah rupa geometri itu.

Sumber