Jaga responsivitas penggambaran persegi panjang yang cepat dengan memisahkan pratinjau seret aktif dari bentuk kanvas yang sudah ditetapkan. Dalam aplikasi gambar Mac, biarkan satu permukaan gestur menangani input, simpan geometri sementara secara lokal, dan tambahkan ke koleksi bentuk hanya saat seretan berhasil diselesaikan.

Panduan ini ditujukan bagi pengembang yang membuat alat persegi panjang sederhana dengan klik dan seret di aplikasi macOS mereka sendiri. Semua bentuk contoh disimpan di memori. Tujuannya adalah mencegah gerakan penunjuk berulang kali mengubah seluruh model gambar, lalu menggunakan pemrofilan untuk mengidentifikasi biaya rendering yang masih tersisa.

Mengapa menggambar melambat saat bentuk yang sudah ditetapkan bertambah?

Mulailah dengan membedakan tiga operasi: menerima input, memperbarui status, dan merender geometri. Seretan dapat sering mengubah geometri sementara. Jika perubahan itu juga mengganti koleksi yang sudah ditetapkan, membangun ulang setiap jalur, atau membatalkan validitas hierarki tampilan yang luas, pekerjaan untuk setiap gerakan dapat bertambah seiring gambar.

Apple menjelaskan bahwa SwiftUI melacak dependensi dan memperbarui tampilan saat dependensi tersebut berubah. Pembaruan tampilan individual yang lambat maupun banyaknya pembaruan yang tidak perlu dapat menyebabkan tenggat waktu frame terlewat. Memisahkan status pratinjau yang sering berubah dari konten yang sudah ditetapkan merupakan penerapan arsitektural dari model yang didokumentasikan tersebut. Optimalkan performa SwiftUI dengan Instruments

Canvas menyediakan penggambaran mode langsung melalui konteks grafis. Apple menyebutnya sebagai kemungkinan peningkatan performa untuk gambar dinamis yang kompleks, tetapi tidak menyediakan interaksi atau aksesibilitas untuk setiap elemen yang digambar. Lampirkan penanganan input ke permukaan gambar yang dirancang dengan sengaja; implementasikan dukungan seleksi dan aksesibilitas terpisah jika aplikasi nantinya memerlukan bentuk yang dapat dioperasikan secara individual. Canvas

Karena itu, batas yang berguna harus dinyatakan dengan jelas: permukaan input menangani gestur, status sementara menangani pratinjau, dan tampilan induk memiliki persegi panjang yang sudah ditetapkan. Memisahkan tanggung jawab ini mengurangi dependensi yang dapat dihindari. Ini tidak menjamin sistem tidak akan pernah menggambar ulang kanvas yang sudah ditetapkan.

Pisahkan pratinjau dari gambar yang sudah ditetapkan

Gunakan rencana kepemilikan ini sebelum memilih optimasi rendering:

Tanggung jawab Pemilik Kapan berubah
Persegi panjang seretan saat ini Permukaan gestur Selama seretan aktif
Koleksi persegi panjang yang sudah ditetapkan Tampilan gambar induk Sekali setelah setiap seretan berhasil
Menggambar persegi panjang yang sudah ditetapkan Kanvas latar Saat inputnya memerlukan pembaruan
Pengenalan penunjuk Permukaan gestur Sepanjang rangkaian gestur

Untuk geometri sementara, @GestureState berguna karena SwiftUI mengatur ulangnya saat gestur tidak aktif. Tetapkan melalui onEnded, yang callback-nya menerima nilai gestur akhir. Apple menetapkan bahwa callback ini hanya dijalankan saat gestur berakhir dengan sukses; status gestur menangani pembersihan apa pun cara berakhirnya gestur. GestureState, onEnded(_:)

Untuk persegi panjang, geometri akhir hanya memerlukan lokasi awal dan lokasi saat ini. Tidak perlu menyimpan setiap lokasi penunjuk antara. Apple mendokumentasikan posisi tersebut secara terpisah dari titik akhir prediksi; gunakan lokasi akhir sebenarnya saat menetapkan persegi panjang. DragGesture.Value

Bangun alat persegi panjang lengkap di memori

Dua blok kode berikut membentuk satu berkas sumber Swift lengkap. Dalam proyek App SwiftUI macOS yang menargetkan macOS 12 atau lebih baru, ganti berkas entri aplikasi yang dihasilkan dengan kedua blok ini dan hapus deklarasi entri aplikasi lainnya. Canvas tersedia mulai macOS 12. Contoh ini telah diperiksa, tetapi tidak dapat dikompilasi di sini karena lingkungan ini tidak memiliki toolchain Swift. Canvas

Blok pertama mendefinisikan aplikasi, koleksi yang sudah ditetapkan, dan perendernya. Induk hanya menerima persegi panjang yang telah selesai; induk tidak membaca status seretan aktif.

import SwiftUI

@main
struct RectangleDrawingApp: App {
    var body: some Scene {
        WindowGroup {
            DrawingBoard()
        }
    }
}

struct DrawingBoard: View {
    @State private var rectangles: [CGRect] = []

    var body: some View {
        VStack(spacing: 8) {
            Text("Drag to draw a rectangle")
            Text("Committed rectangles: \(rectangles.count)")

            ZStack {
                Color.white

                CommittedRectangles(rectangles: rectangles)
                    .allowsHitTesting(false)

                RectangleGestureSurface { rectangle in
                    rectangles.append(rectangle)
                }
            }
            .clipped()
            .frame(minWidth: 480, minHeight: 320)
        }
        .padding()
    }
}

struct CommittedRectangles: View {
    let rectangles: [CGRect]

    var body: some View {
        Canvas { context, _ in
            for rectangle in rectangles {
                context.stroke(
                    Path(rectangle),
                    with: .color(.blue),
                    lineWidth: 2
                )
            }
        }
    }
}

Tambahkan blok berikut ke berkas yang sama. Permukaan transparan menutupi area gambar, memberi pengujian hit bentuk persegi panjang yang eksplisit, dan menjadi pemilik satu-satunya gestur gambar. Apple mendokumentasikan contentShape sebagai penentu bentuk pengujian hit dan allowsHitTesting(false) sebagai pengecualian tampilan dari pengujian hit. contentShape(_:eoFill:), allowsHitTesting(_:)

struct RectangleGestureSurface: View {
    let commit: (CGRect) -> Void
    @GestureState private var preview: CGRect? = nil

    var body: some View {
        ZStack {
            Color.clear

            Canvas { context, _ in
                if let rectangle = preview {
                    context.stroke(
                        Path(rectangle),
                        with: .color(.orange),
                        lineWidth: 2
                    )
                }
            }
            .allowsHitTesting(false)
        }
        .contentShape(Rectangle())
        .gesture(
            DragGesture(minimumDistance: 0, coordinateSpace: .local)
                .updating($preview) { value, state, _ in
                    state = Self.rectangle(
                        from: value.startLocation,
                        to: value.location
                    )
                }
                .onEnded { value in
                    let rectangle = Self.rectangle(
                        from: value.startLocation,
                        to: value.location
                    )
                    commit(rectangle)
                }
        )
    }

    private static func rectangle(
        from start: CGPoint,
        to end: CGPoint
    ) -> CGRect {
        CGRect(
            x: min(start.x, end.x),
            y: min(start.y, end.y),
            width: abs(end.x - start.x),
            height: abs(end.y - start.y)
        )
    }
}

minimumDistance: 0 menghapus persyaratan jarak minimum untuk pengenalan. Ruang koordinat lokal menjaga agar posisi gestur selaras dengan kanvas pratinjau yang berukuran sama. Ini adalah opsi konfigurasi gestur yang terdokumentasi, bukan jaminan tentang frekuensi kejadian atau latensi tampilan. DragGesture

Helper persegi panjang menormalisasi keempat arah seretan. Sebagai pemeriksaan ilustratif, menyeret dari (90, 80) ke (30, 20) menghasilkan origin (30, 20) dan ukuran (60, 60). Pratinjau dan penetapan menggunakan perhitungan yang sama, sehingga melepas penunjuk tidak mengubah rumus geometri.

Alat minimal ini juga menetapkan persegi panjang dengan lebar nol dan tinggi nol. Karena itu, sebuah klik dapat menambah jumlah tanpa menghasilkan persegi panjang yang terlihat. Jika produk memerlukan ukuran minimum, tambahkan aturan tersebut secara eksplisit saat penetapan dan berikan umpan balik yang sesuai kepada pengguna. Contoh ini menyimpan setiap gestur yang berhasil diselesaikan, alih-alih diam-diam menyaring hasil berukuran kecil.

Area yang terlihat dipotong, sedangkan geometri yang tersimpan tetap tidak berubah. Karena itu, persegi panjang yang melampaui papan dapat memiliki koordinat di luar papan. Tentukan apakah perubahan ukuran di masa mendatang perlu menampilkan geometri tersebut sebelum menambahkan pembatasan koordinat.

Diagnosis biaya yang tersisa sebelum menambahkan cache

Contoh ini menghapus mutasi model yang sudah ditetapkan dari jalur pembaruan seretan. Perender yang sudah ditetapkan masih menelusuri koleksi setiap kali perender tersebut dijalankan. Bedakan pengurangan propagasi status dari pengurangan pekerjaan rendering: tampilan terpisah bukan cache gambar persisten yang terdokumentasi.

Gunakan perbandingan terkontrol untuk menemukan biaya yang masih ada. Pilih populasi dalam memori yang bersifat ilustratif, seperti 0, 500, dan 5.000 persegi panjang. Pertahankan dimensi persegi panjang, ukuran jendela, skala tampilan, dan konfigurasi build agar konsisten. Jumlah ini adalah input pengujian, bukan batas kapasitas atau hasil pengukuran.

Catat tiga interval secara terpisah: menyeret saat koleksi tidak berubah, melepas untuk menetapkan bentuk, dan mengubah ukuran jendela. Perlambatan sepanjang seretan menunjukkan jenis pekerjaan yang berbeda dari jeda yang hanya terjadi saat penetapan. Perilaku saat ukuran berubah dapat mengungkap pembangunan ulang geometri atau cache yang tidak terjadi dalam penggambaran biasa.

Sesi pemrofilan SwiftUI dari Apple menjelaskan alat Long View Body Updates, Long Representable Updates, dan Cause & Effect Graph, bersama Time Profiler. Gunakan grafik dependensi untuk menyelidiki alasan tampilan yang sudah ditetapkan diperbarui selama seretan; gunakan sampel CPU untuk menemukan konstruksi geometri atau pekerjaan rendering yang mahal. Apple memperkenalkan alur kerja instrumen tersebut di Instruments 26. Optimalkan performa SwiftUI dengan Instruments

Untuk jeda yang lebih luas, sesi WWDC26 tentang responsivitas dari Apple memulai dengan Time Profiler dan membedakan eksekusi sibuk dari waktu yang dihabiskan untuk menunggu. Gunakan pembedaan tersebut sebelum memindahkan pekerjaan antar executor atau mengganti stack rendering. Profilkan, perbaiki, dan verifikasi: Tingkatkan responsivitas app dengan Instruments

Panduan keputusan berikut merupakan inferensi rekayasa dari alat pemrofilan tersebut dan rancangan kepemilikan di atas:

Perilaku yang diamati Selidiki terlebih dahulu Perubahan yang dapat dicoba
Status gambar induk berubah saat penunjuk bergerak Callback pembaruan gestur dan status bersama Simpan status pratinjau di dalam permukaan gestur
Gambar yang sudah ditetapkan mendominasi sampel selama gerakan Penelusuran berulang dan pembuatan jalur Prahitung geometri atau perkenalkan cache konten yang sudah ditetapkan
Jeda saat melepas semakin panjang seiring bertambahnya koleksi Mutasi array dan rekonstruksi gambar penuh Kurangi snapshot yang disimpan dan bangun ulang hanya konten turunan yang terdampak
Penggambaran persegi panjang responsif, tetapi sapuan bebas panjang lambat Ukuran jalur aktif dan rekonstruksi Profilkan sapuan aktif yang terus bertambah secara terpisah
Jeda terutama terjadi saat ukuran berubah Rendering yang bergantung pada ukuran dan invalidasi cache Tentukan perilaku eksplisit saat ukuran berubah untuk konten turunan

Cache menambahkan batas kepemilikan lain: geometri persegi panjang tetap menjadi sumber kebenaran, sementara cache adalah konten turunan yang dapat diganti. Sertakan ukuran gambar, skala tampilan, dan gaya yang relevan dalam rancangan invalidasi. Untuk gambar yang hanya ditambahkan, pembaruan cache inkremental merupakan kandidat optimasi; pengeditan dan penghapusan memerlukan strategi pembangunan ulang atau invalidasi wilayah yang jelas. Verifikasi hasilnya dengan populasi input yang sama sebelum mempertahankan kompleksitas tambahan tersebut.

Periksa penyalinan array pada batas penetapan

Array Swift memiliki semantik nilai dan menggunakan penyimpanan salin-saat-ditulis. Salinan dapat berbagi penyimpanan hingga mutasi mengharuskan penyimpanan terpisah; array yang berisi instance kelas tetap berbagi referensi ke instance tersebut. Baik meneruskan array ke tampilan maupun menambahkan elemen ke array tidak boleh digambarkan seolah selalu menyalin setiap elemen. Array

Untuk alat ini, hindari menyimpan snapshot gambar penuh yang baru pada setiap pembaruan penunjuk. Jika pemrofilan menemukan penyalinan saat penetapan, periksa snapshot mana yang masih hidup dan bagaimana model gambar dibagikan. Mengganti tipe nilai dengan objek mutable bersama semata-mata untuk menghindari penyalinan mengubah semantik kepemilikan dan memerlukan keputusan desain tersendiri.

Perlakukan presentasi asinkron sebagai opsi yang perlu diukur

Apple mendefinisikan rendersAsynchronously sebagai cara agar kanvas dapat menampilkan kontennya kepada induk secara asinkron. Deskripsi tersebut tidak menjanjikan pengiriman gestur yang lebih cepat, tidak memindahkan penangan gestur ke executor latar belakang, dan tidak menghilangkan penelusuran koleksi. Evaluasilah secara terpisah setelah batas input dan status ditetapkan. rendersAsynchronously

Kapan AppKit sebaiknya menjadi pemilik permukaan gambar?

Gunakan permukaan AppKit khusus saat tugas memerlukan penanganan metode responder langsung atau invalidasi wilayah gambar yang eksplisit. Apple mendokumentasikan NSView sebagai infrastruktur untuk menggambar dan menangani kejadian, serta mendukung subclass untuk perilaku khusus. Ini merupakan alternatif yang didukung dengan tanggung jawab rendering tersendiri. NSView

Untuk implementasi AppKit, pertahankan urutan yang sama: inisialisasi geometri sementara di mouseDown(with:), perbarui di mouseDragged(with:), lalu tetapkan di mouseUp(with:). Konversikan event.locationInWindow ke koordinat tampilan penerima sebelum menghitung geometri. Panduan kejadian mouse yang diarsipkan Apple mendokumentasikan urutan dan konversi koordinat ini; referensi NSView saat ini mendokumentasikan metode responder yang didukung. Menangani Kejadian Mouse, NSView

Minta tampilan diperbarui dengan mengatur needsDisplay = true; Apple menetapkan bahwa tampilan kemudian dirender ulang pada giliran event loop berikutnya. Untuk rancangan berbasis wilayah, invalidasikan gabungan batas pratinjau lama dan baru, dengan perluasan untuk mencakup goresan. Invalidasi wilayah hanya mengurangi penelusuran jika implementasi gambar juga menghindari pemrosesan konten yang tidak terdampak. needsDisplay, Menangani Kejadian Mouse

Sematkan permukaan dengan NSViewRepresentable: buat di makeNSView, selaraskan status eksternal di updateNSView, dan gunakan coordinator untuk mengomunikasikan perubahan kembali ke SwiftUI. Perbarui perutean callback selama pembaruan, dan hindari mengatur ulang draf aktif hanya karena SwiftUI memberikan nilai representable lainnya. Apple juga menyatakan bahwa SwiftUI mengendalikan tata letak tampilan yang direpresentasikan; jangan langsung menimpa frame atau bounds yang dikelolanya. NSViewRepresentable

Pilih satu pemilik untuk input gambar. Jika AppKit menangani rangkaian penggambaran, jangan juga menetapkan rangkaian tersebut melalui gestur seret SwiftUI yang bertumpang tindih. Untuk subclass NSView langsung, Apple menginstruksikan implementasi yang menangani kejadian mouse yang tercantum agar tidak memanggil implementasi superclass untuk kejadian yang ditangani tersebut. NSView

Mulailah dengan peningkatan yang kecil dan mudah diamati: status pratinjau lokal, satu batas penetapan, dan perender yang sudah ditetapkan dengan input yang tidak berubah selama gerakan. Lalu bandingkan gambar sintetis yang ukurannya bertambah di Instruments. Jejak yang dihasilkan menunjukkan apakah perubahan berguna berikutnya berkaitan dengan pengelolaan dependensi, kepemilikan koleksi, persiapan geometri, atau rendering.

Sumber