Gabungkan DragGesture, RotateGesture, dan MagnifyGesture dengan simultaneously(with:), lalu tampilkan nilainya melalui satu pipeline transformasi. Untuk kanvas Mac, biarkan permukaan input tetap diam dan simpan perubahan sementara setiap gestur secara terpisah dari hasil yang telah diterapkan.

Tutorial ini membuat penampil bentuk sederhana di dalam app macOS Anda sendiri: klik dan seret untuk menggeser, cubit untuk memperbesar, dan gunakan gestur rotasi trackpad untuk memutar bentuk. Tutorial ini hanya menggunakan persegi panjang dan lingkaran di memori. Komposisi ini memungkinkan gestur yang dikenali terjadi secara bersamaan; input mana yang benar-benar tumpang tindih bergantung pada peristiwa yang dikirim perangkat.

Apa arti simultan di Mac?

simultaneously(with:) dari Apple menggabungkan dua gestur menjadi gestur lain, sehingga hasilnya dapat digabungkan dengan gestur ketiga. Ini menyediakan komposisi yang diperlukan untuk pan, rotasi, dan zoom tanpa memerlukan tekan lama atau tindakan pendahuluan lainnya. Dokumentasi gestur simultan Apple menjelaskan kombinasi ini.

Tentukan arti pan sebelum menulis kode. Di sini, pan berarti menyeret dengan tombol mouse utama ditekan, termasuk klik-seret pada trackpad. Menggeser dua jari melintasi trackpad biasanya menghasilkan input gulir. Panduan peristiwa Apple yang diarsipkan secara eksplisit membedakan aliran gulir tersebut dari gestur pembesaran dan rotasi; dokumentasi AppKit saat ini juga menyediakan gulir melalui scrollWheel(with:). Menambahkan gestur seret tidak mengubah gulir menjadi seretan. Menangani Peristiwa Trackpad, scrollWheel(with:)

Gunakan tabel keputusan ini untuk memilih lapisan input bagi kanvas Anda:

Interaksi yang diinginkan Titik awal Batasan yang perlu dipertahankan
Pan dengan klik-seret, zoom dengan cubitan, rotasi trackpad Contoh SwiftUI di bawah Komposisi memungkinkan aliran gestur yang dikenali saling tumpang tindih.
Pan dengan gulir dua jari Kanvas AppKit yang disematkan di SwiftUI Gulir tetap berupa aliran peristiwa gulir.
Transformasi tiga komponen yang dijamin dari satu pola jari Interaksi khusus yang spesifikasinya ditetapkan terpisah Komposisi simultan saja tidak memastikan bahwa perangkat keras atau macOS akan mengirim setiap komponen.

Contoh ini memerlukan macOS 14 atau lebih baru, yang menyediakan MagnifyGesture dan RotateGesture. Contoh ini menggunakan inisialisator dasar keduanya, bukan API konfigurasi jenis input yang lebih baru. MagnifyGesture, RotateGesture

Bagaimana sebaiknya status transformasi disusun?

Berikan setiap komponen nilai yang telah diterapkan dan nilai gestur yang sedang berlangsung. Translasi dan rotasi digabungkan dengan penjumlahan; skala digabungkan dengan perkalian. Persamaan berikut mendefinisikan perilaku contoh ini, bukan persyaratan dari SwiftUI:

translasi yang ditampilkan = translasi yang diterapkan + translasi seret yang sedang berlangsung
rotasi yang ditampilkan    = rotasi yang diterapkan + rotasi yang sedang berlangsung
skala yang ditampilkan    = batasi(skala yang diterapkan × pembesaran yang sedang berlangsung)

Gunakan @GestureState untuk nilai yang sedang berlangsung. Apple mendokumentasikan bahwa nilainya diatur ulang saat gestur menjadi tidak aktif. Terapkan setiap komponen melalui callback onEnded masing-masing, yang hanya dijalankan setelah gestur berhasil selesai. Dengan begitu, pembatalan memiliki arti yang jelas: buang pratinjau yang belum selesai untuk komponen tersebut dan pertahankan nilai sebelumnya yang telah diterapkan. GestureState, onEnded(_:)

Tempatkan callback ini pada gestur masing-masing sebelum menggabungkannya. Dengan demikian, setiap callback menerima nilai bertipe miliknya sendiri dan hanya mengubah properti yang menjadi tanggung jawabnya. Tidak ada snapshot transformasi awal bersama yang dapat ditimpa oleh gestur yang dimulai belakangan.

Translasi seret perlu diperhatikan secara khusus: nilainya adalah total pergerakan sejak gestur dimulai. Menambahkan total itu ke offset yang telah diterapkan pada setiap pembaruan akan menghitung ulang pergerakan sebelumnya. Tetapkan total tersebut ke pratinjau sementara, lalu tambahkan total akhir satu kali saat gestur selesai. DragGesture.Value.translation

Buat contoh SwiftUI lengkap

Buat app SwiftUI macOS dengan target deployment macOS 14 atau lebih baru. Ganti entri app dan tampilan konten yang dibuat secara otomatis dengan satu file berikut; pastikan hanya ada satu deklarasi @main di target. Tidak diperlukan aset, paket, atau data eksternal.

Kanvas abu-abu menangani gestur. Bentuk di dalamnya menerima transformasi visual, sehingga menggeser atau memutar bentuk tidak mentransformasi permukaan input gestur. Contoh ini sengaja memperbesar dan memutar bentuk pada titik tengahnya.

import SwiftUI

@main
struct TransformCanvasApp: App {
    var body: some Scene {
        WindowGroup {
            TransformCanvas()
                .frame(minWidth: 400, minHeight: 300)
        }
        .defaultSize(width: 720, height: 500)
    }
}

struct TransformCanvas: View {
    @State private var offset = CGSize.zero
    @State private var angle = Angle.zero
    @State private var scale: CGFloat = 1

    @GestureState private var liveOffset = CGSize.zero
    @GestureState private var liveAngle = Angle.zero
    @GestureState private var liveScale: CGFloat = 1

    private func boundedScale(_ value: CGFloat) -> CGFloat {
        min(max(value, 0.25), 4)
    }

    private var pan: some Gesture {
        DragGesture(minimumDistance: 3, coordinateSpace: .local)
            .updating($liveOffset) { value, state, _ in
                state = value.translation
            }
            .onEnded { value in
                offset = CGSize(
                    width: offset.width + value.translation.width,
                    height: offset.height + value.translation.height
                )
            }
    }

    private var rotation: some Gesture {
        RotateGesture()
            .updating($liveAngle) { value, state, _ in
                state = value.rotation
            }
            .onEnded { value in
                angle = angle + value.rotation
            }
    }

    private var zoom: some Gesture {
        MagnifyGesture()
            .updating($liveScale) { value, state, _ in
                state = value.magnification
            }
            .onEnded { value in
                scale = boundedScale(scale * value.magnification)
            }
    }

    private var manipulation: some Gesture {
        pan.simultaneously(with: rotation)
            .simultaneously(with: zoom)
    }

    var body: some View {
        ZStack {
            Color.gray.opacity(0.12)

            RoundedRectangle(cornerRadius: 18)
                .fill(Color.blue)
                .overlay(alignment: .topLeading) {
                    Circle()
                        .fill(Color.white)
                        .frame(width: 24, height: 24)
                        .padding(16)
                }
                .frame(width: 180, height: 120)
                .scaleEffect(
                    boundedScale(scale * liveScale),
                    anchor: .center
                )
                .rotationEffect(angle + liveAngle, anchor: .center)
                .offset(CGSize(
                    width: offset.width + liveOffset.width,
                    height: offset.height + liveOffset.height
                ))
                .allowsHitTesting(false)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .contentShape(Rectangle())
        .clipped()
        .gesture(manipulation)
    }
}

Apple mendokumentasikan tipe gestur seret, pembesaran, dan rotasi yang mendasarinya, serta cara memasang gestur ke tampilan. Contoh ini menggabungkan API tersebut dengan kebijakan status dan koordinat yang eksplisit. SwiftUI tidak tersedia di lingkungan riset, sehingga program ini diperiksa berdasarkan dokumentasi, tetapi tidak dikompilasi atau dijalankan di sini. DragGesture, MagnifyGesture, RotateGesture

Mengapa skala, lalu rotasi, lalu offset?

Pemetaan yang dimaksud adalah memperbesar atau memperkecil bentuk dari titik tengahnya, memutarnya pada titik tengah yang sama, lalu menggesernya dalam koordinat kanvas. Menerapkan offset terakhir menjaga agar pan berada di luar skala dan rotasi bentuk. Menggerakkan penunjuk secara horizontal seharusnya menggeser bentuk secara horizontal meskipun bentuk itu telah berputar.

Urutan ini merupakan pilihan desain berdasarkan pipeline transformasi contoh. Apple mendokumentasikan bahwa scaleEffect menskalakan keluaran yang dirender di sekitar suatu titik acuan, rotationEffect memutar keluaran yang dirender, dan offset menggeser konten yang ditampilkan. Efek visual ini tidak mengubah kanvas yang diam menjadi wadah tata letak yang ikut ditransformasi. scaleEffect(_:anchor:), rotationEffect(_:anchor:), offset(_:)

Batas 0,25 dan 4 adalah pengaturan app sebagai contoh. Pembatasan yang sama dijalankan saat pratinjau dan penerapan, sehingga skala yang ditampilkan tetap sama setelah gestur dilepas. min dan max Swift memilih nilai terkecil dan terbesar untuk menyatakan batas tersebut. min(::), max(::)

Periksa interaksi dengan ekspektasi konkret

Mulailah dengan mencoba tiap komponen secara terpisah, lalu coba tumpang tindih menggunakan perangkat input yang tersedia. Lingkaran putih membuat rotasi terlihat meskipun persegi panjang berukuran kecil.

  1. Klik dan seret pada kanvas, lepaskan, lalu seret lagi. Seretan kedua seharusnya dimulai dari posisi yang telah diterapkan oleh seretan pertama.
  2. Cubit untuk memperbesar bentuk, lepaskan, lalu cubit lagi. Setiap gestur seharusnya mengalikan skala yang tersimpan.
  3. Putar, lepaskan, lalu putar lagi. Sudutnya seharusnya terakumulasi.
  4. Setelah memperbesar dan memutar, seret secara horizontal. Bentuk seharusnya tetap bergerak sepanjang sumbu horizontal kanvas.
  5. Jika perangkat mengirim input yang tumpang tindih, akhiri satu komponen sambil melanjutkan komponen lainnya. Komponen yang masih berlangsung seharusnya terus memperbarui propertinya sendiri.
  6. Hentikan gestur aktif, misalnya dengan beralih aplikasi. Gestur apa pun yang menjadi tidak aktif tanpa berhasil selesai seharusnya kehilangan pratinjau sementaranya, bukan membiarkan status sementara tertahan.

Sebagai pemeriksaan aritmetika, anggap skala yang tersimpan adalah 1,5 dan cubitan melaporkan 1,2. Hasil yang ditampilkan adalah 1,8. Jika sudut yang tersimpan adalah 30 derajat dan rotasi yang sedang berlangsung adalah 15 derajat, sudut yang ditampilkan adalah 45 derajat. Translasi seret sebesar 40 poin tetap menjadi offset kanvas 40 poin, terlepas dari skala dan sudut tersebut.

Contoh ini menerapkan setiap komponen secara terpisah. Jika rotasi berhasil selesai lalu seretan yang masih aktif dibatalkan, rotasi yang selesai tetap berlaku sementara seretan yang belum selesai dibatalkan. Editor yang memerlukan satu operasi atomik untuk semua komponen membutuhkan model sesi interaksi tambahan dengan kebijakan penerapan dan pembatalan yang eksplisit.

Bentuk juga dapat diseret ke luar kanvas yang terlihat karena translasi sengaja tidak dibatasi. Kanvas tetap dapat diseret, sehingga bentuk dapat digerakkan kembali ke arah sebaliknya. Penampil yang sudah selesai dapat menambahkan perintah reset terpisah atau membatasi translasi sesuai batas kontennya.

Kapan sebaiknya menggunakan kanvas AppKit?

Pilih lapisan input AppKit jika tugas secara khusus memerlukan pan dengan gulir dua jari atau kontrol langsung atas momentum gulir. Sematkan tampilan AppKit yang dikelola melalui NSViewRepresentable, yang disediakan Apple untuk membuat, memperbarui, dan membongkar tampilan AppKit di dalam SwiftUI. NSViewRepresentable

Tetapkan satu pemilik untuk setiap aliran input. Di kanvas AppKit tersebut, tangani gulir melalui scrollWheel(with:) dan gunakan recognizer untuk pembesaran dan rotasi. Teruskan perubahan yang dihasilkan ke model translasi, sudut, dan skala yang sama. Hindari memasang handler SwiftUI lain yang juga menerapkan input yang sama ke model tersebut.

Untuk recognizer AppKit yang seharusnya dapat berjalan bersamaan, implementasikan gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) dan kembalikan true hanya untuk pasangan recognizer yang dimaksud dan menjadi bagian dari kanvas Anda. Apple mendokumentasikan bahwa metode delegate ini mengizinkan pengenalan simultan; metode ini tidak menciptakan aliran input yang tidak tersedia. Metode delegate pengenalan simultan

Pertahankan konvensi nilai saat menjembatani API. Recognizer pembesaran AppKit melaporkan nilai yang harus ditambah 1 untuk memperoleh faktor skala, sedangkan recognizer rotasinya menampilkan radian. Jangan menyalin penetapan nilai SwiftUI tanpa perubahan. NSMagnificationGestureRecognizer.magnification, NSRotationGestureRecognizer.rotation

Untuk gulir, Apple menyebut scrollingDeltaX sebagai properti delta horizontal yang disarankan dan mencatat bahwa delta yang tidak presisi mungkin perlu disesuaikan. Dokumentasi momentumnya menjelaskan bahwa peristiwa gulir berlanjut setelah gestur fisik berhenti. Tentukan apakah kanvas menerima kelanjutan tersebut, lalu teruskan setiap peristiwa yang diterima satu kali. scrollingDeltaX, momentumPhase

Bridge ini seharusnya mengubah transformasi konten kanvas Anda, sementara SwiftUI tetap mengelola tata letak tampilan yang direpresentasikan. Apple secara eksplisit memperingatkan agar Anda tidak mengatur frame atau bounds tampilan yang direpresentasikan secara langsung. Gunakan coordinator representable untuk membawa perubahan delegate dan target-action kembali ke SwiftUI. NSViewRepresentable

Untuk penampil klik-seret, implementasi SwiftUI ini menyediakan titik awal yang lengkap: satu permukaan input yang diam, tiga komponen gestur yang dikelola secara terpisah, dan urutan transformasi yang disengaja. Pertahankan batasan tersebut saat mengembangkannya, dan pilih jalur AppKit jika tugas yang dihadapi pengguna memerlukan pan dengan gulir.

Sumber