Untuk mengurutkan ulang daftar di app Mac Anda, teruskan sumber dan tujuan onMove SwiftUI langsung ke move(fromOffsets:toOffset:). Tujuan menandai batas dalam koleksi asli, sehingga mengurangi satu secara manual untuk pemindahan ke bawah menghasilkan hasil yang keliru.

Panduan ini membuat daftar macOS native sederhana dengan label sintetis, menjelaskan aritmetika tujuan, dan menyediakan matriks pengujian untuk mendiagnosis baris yang salah tempat. Cakupannya adalah mengurutkan ulang item di dalam daftar milik app Anda sendiri.

Apa arti tujuan onMove?

Apple mendefinisikan dua argumen onMove(perform:) sebagai offset relatif terhadap koleksi yang mendasari tampilan dinamis. Argumen pertama adalah IndexSet berisi offset sumber; argumen kedua adalah offset tujuan. Posisi-posisi ini merujuk pada koleksi yang digunakan oleh ForEach tersebut. Dokumentasi onMove(perform:) Apple.

Untuk move(fromOffsets:toOffset:), Apple mendefinisikan tujuan sebagai offset sebelum elemen yang dipindahkan harus disisipkan, dan count juga dapat berarti akhir. Anggaplah itu sebagai batas di antara elemen dalam koleksi asli. Dokumentasi move(fromOffsets:toOffset:) Apple.

Perhatikan urutan awal A, B, C, D, E. Offset elemennya adalah 0, 1, 2, 3, 4, sedangkan batas penyisipannya berkisar dari 0 hingga 5. Batas 4 tepat sebelum E; batas 5 berada setelah item terakhir.

Karena itu, pemindahan B dari offset sumber 1 ke tujuan 4 menghasilkan A, C, D, B, E. Item yang dipindahkan berakhir pada indeks 3, meskipun argumen tujuannya adalah 4. Kedua nilai menggunakan posisi berbasis nol; keduanya menjelaskan tahap operasi yang berbeda.

Perbedaan itu menjelaskan kesalahan umum saat memindahkan item ke bawah. Kode yang mengubah tujuan 4 menjadi 3 sebelum memanggil metode pemindahan koleksi meminta batas yang berbeda dan menempatkan B sebelum D. Teruskan nilai callback apa adanya saat menggunakan metode pemindahan Apple.

Buat daftar macOS lengkap yang dapat diurutkan ulang

Gunakan proyek app SwiftUI native macOS di Xcode. Contoh lengkap di bawah menargetkan macOS 11 atau lebih baru karena menggunakan siklus hidup App SwiftUI; Apple mencantumkan onMove sendiri sebagai tersedia sejak macOS 10.15. Ganti deklarasi app dan tampilan konten yang dibuat otomatis dengan kode ini, dan pastikan hanya ada satu titik masuk @main di target. Dokumentasi App Apple, dokumentasi onMove(perform:) Apple.

import SwiftUI
import Foundation

struct DemoItem: Identifiable {
    let id: Int
    let label: String

    static let samples: [DemoItem] = [
        DemoItem(id: 0, label: "A"),
        DemoItem(id: 1, label: "B"),
        DemoItem(id: 2, label: "C"),
        DemoItem(id: 3, label: "D"),
        DemoItem(id: 4, label: "E")
    ]
}

@main
struct ReorderDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ReorderView()
        }
    }
}

struct ReorderView: View {
    @State private var items = DemoItem.samples
    @State private var selection = Set<Int>()
    @State private var lastMove = "No move yet"

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Drag rows to change their order")

            List(selection: $selection) {
                ForEach(items) { item in
                    Text(item.label)
                        .tag(item.id)
                }
                .onMove(perform: moveItems)
            }

            Text("Order: " + items.map(\.label).joined(separator: ", "))
            Text(lastMove)

            Button("Reset") {
                items = DemoItem.samples
                selection.removeAll()
                lastMove = "No move yet"
            }
        }
        .padding()
        .frame(minWidth: 420, minHeight: 300)
    }

    private func moveItems(from source: IndexSet, to destination: Int) {
        lastMove = "Source: \(Array(source)); destination: \(destination)"
        items.move(fromOffsets: source, toOffset: destination)
    }
}

ForEach merender array yang sama dengan yang diubah oleh handler. Setiap item memiliki pengenal tetap yang tidak bergantung pada posisinya saat ini. Apple mensyaratkan elemen yang dapat diidentifikasi atau kunci identitas eksplisit untuk ForEach; mempertahankan setiap pengenal saat pemindahan memungkinkan tampilan tetap mewakili item yang sama. Dokumentasi ForEach Apple.

Array tersebut adalah state lokal tampilan. Mengubahnya menyebabkan SwiftUI memperbarui tampilan yang bergantung padanya, termasuk daftar dan urutan yang ditampilkan. Contoh ini menyimpan urutan di memori dan tidak menyertakan implementasi persistensi. Dokumentasi State Apple.

Jalankan app, seret B ke batas tepat sebelum E, lalu periksa urutan dan tampilan callback. Gunakan Reset sebelum mencoba skenario lain. Binding seleksi eksplisit juga mendukung pemilihan beberapa baris; Apple mendokumentasikan set seleksi dan seleksi Mac tanpa memasuki mode edit. Dokumentasi List Apple.

Jangan tambahkan EditButton() ke contoh macOS native ini. Informasi ketersediaan Apple saat ini mencakup iOS dan Mac Catalyst, tetapi tidak mencakup macOS native. Kontrol pengeditan dari tutorial seluler tidak boleh disalin ke target Mac tanpa memeriksa ketersediaannya. Dokumentasi EditButton Apple.

Bagaimana cara menghitung indeks penyisipan akhir?

Anda hanya memerlukan perhitungan ini saat menerapkan penghapusan dan penyisipan sendiri, atau saat menerjemahkan pemindahan ke operasi model lain. Ini adalah model penjelas yang diturunkan dari semantik penyisipan yang didokumentasikan Apple, bukan penyesuaian tambahan yang harus diterapkan sebelum memanggil metode pemindahan bawaan.

Perhitungannya adalah:

indeks penyisipan setelah penghapusan = tujuan asli
                                      - jumlah offset sumber di bawah tujuan

Untuk sumber {1} dan tujuan 4, satu elemen yang dihapus semula berada sebelum batas. Array yang tersisa adalah A, C, D, E, dan indeks penyisipannya adalah 4 - 1 = 3. Menyisipkan B di sana menghasilkan A, C, D, B, E.

Jika ada beberapa sumber, satu koreksi ke bawah tidak memadai. Dengan sumber {1, 3} dan tujuan 5, item yang dipindahkan adalah B, D; array yang tersisa adalah A, C, E. Dua offset sumber berada sebelum tujuan 5, sehingga indeks penyisipannya adalah 3. Hasilnya adalah A, C, E, B, D.

Implementasi kustom harus terlebih dahulu menangkap elemen yang dipindahkan dalam urutan aslinya, lalu menghapus elemen asal tanpa membiarkan penghapusan sebelumnya membatalkan posisi sumber berikutnya. Setelah itu, implementasi dapat menghitung posisi penyisipan yang telah disesuaikan dan menyisipkan blok yang ditangkap sekaligus. Untuk daftar berbasis array ini, metode bawaan Apple menghindarkan Anda dari penerapan langkah-langkah tersebut sendiri.

Kasus tepi apa yang harus diuji?

Kasus berikut adalah perhitungan ilustratif untuk urutan awal A, B, C, D, E. Pulihkan urutan tersebut sebelum setiap kasus. Kolom terakhir adalah hasil model yang diharapkan, terlepas dari tampilan indikator pelepasan.

Offset sumber Tujuan asli Indeks penyisipan setelah penghapusan Urutan yang diharapkan
{1} 4 3 A, C, D, B, E
{3} 1 1 A, D, B, C, E
{1} 5 4 A, C, D, E, B
{1} 1 1 A, B, C, D, E
{1} 2 1 A, B, C, D, E
{1, 3} 5 3 A, C, E, B, D
{1, 3} 3 2 A, C, B, D, E

Dua kasus tanpa perubahan untuk satu item penting: menyisipkan item pada batasnya sendiri atau batas sesudahnya tidak mengubah urutannya. Apple mendokumentasikan perilaku ini secara eksplisit. Batas akhir juga valid meskipun tidak dapat digunakan untuk mengakses elemen berdasarkan indeks. Dokumentasi move(fromOffsets:toOffset:) Apple.

Model aritmetika di balik tujuh kasus ini diperiksa dengan perhitungan Python deterministik sederhana. Program SwiftUI telah ditelaah, tetapi tidak dikompilasi atau dijalankan di sini karena toolchain Swift dan runtime macOS tidak tersedia. Verifikasi interaksi seret yang sebenarnya pada versi macOS target Anda.

Mengapa baris masih bisa berpindah secara keliru?

Mulailah dengan batas koleksi, lalu periksa identitas dan mutasi. Urutan ini membedakan masalah aritmetika tujuan dari ketidakcocokan antara baris yang ditampilkan dan data yang tersimpan.

  1. Periksa koleksi yang diteruskan ke ForEach. Jika koleksi yang ditampilkan adalah array hasil filter, offsetnya merujuk pada array yang difilter tersebut. Misalnya, baris terlihat B, D memiliki offset 0, 1; menerapkan sumber 0 ke array penuh akan memindahkan A. Konsekuensi ini mengikuti kontrak callback relatif terhadap koleksi yang didokumentasikan Apple. Dokumentasi onMove(perform:) Apple.
  2. Periksa apakah proses render mengurutkan ulang data. Jika setiap render ulang mengurutkan berdasarkan label, mengubah urutan array dasar tidak dapat mempertahankan urutan tampilan manual. Tentukan kebijakan pengurutan manual sebelum mengaktifkan pengurutan ulang pada tampilan tersebut.
  3. Periksa pengenal. Pertahankan pengenal setiap item saat memindahkannya. Jangan membuat ulang pengenal saat merender atau menggunakan posisi yang berubah sebagai identitas item yang dapat dipindahkan. Dokumentasi ForEach Apple.
  4. Periksa apakah operasi dilakukan lebih dari sekali. Hapus pengurangan tujuan secara manual, lalu pastikan handler menerapkan pemindahan satu kali pada koleksi yang menjadi sumber kebenaran. Pemindahan kedua bekerja pada urutan yang sudah berubah.

Untuk antarmuka yang difilter atau dibagi menjadi beberapa bagian, tentukan perilaku yang diinginkan sebelum menerjemahkan indeks. Putuskan apakah pemindahan hanya mengatur ulang item yang terlihat, mengubah seluruh koleksi, atau memindahkan item antarbagian. Petakan item yang terlihat ke identitas model dan tentukan tujuan sesuai kebijakan tersebut. Offset numerik saja tidak dapat menyatakan semua pilihan itu.

Pertahankan keterkaitan offset callback dengan snapshot koleksi yang dirujuknya. Jika app menunda operasi sementara pengeditan lain mengubah array, tentukan kembali item dan tujuan yang dimaksud alih-alih menggunakan ulang posisi yang sudah usang. Contoh ini menghindari kerumitan tersebut dengan langsung mengubah array lokalnya.

Apakah AppKit atau TextKit mengubah aturan tujuan?

Untuk app yang sudah menggunakan NSTableView, AppKit memiliki kontrak pelepasannya sendiri. NSTableView.DropOperation.above berarti penyisipan di atas baris, sedangkan .on berarti pelepasan pada baris tersebut. Apple mendokumentasikan pelepasan di bawah baris terakhir sebagai baris n dengan operasi .above, dengan n sebagai jumlah baris. Tafsirkan baris dan operasi sebelum menerjemahkan pelepasan AppKit menjadi pemindahan model. Dokumentasi NSTableView.DropOperation Apple.

TextKit menangani penyimpanan teks, tata letak, dan pengeditan melalui komponen seperti NSTextView dan pengelola tata letak teks. TextKit tidak menyediakan kontrak pengurutan ulang array yang digunakan dalam contoh ini. Jika sebuah baris juga memuat editor, pisahkan implementasi pengeditan teks dari operasi koleksi yang mengubah urutan baris. Dokumentasi TextKit Apple.

Untuk daftar Mac yang sederhana, gunakan satu koleksi dengan urutan manual, pertahankan pengenal item, dan teruskan argumen onMove langsung ke metode pemindahan koleksi. Gunakan perhitungan batas dan matriks pengujian saat Anda perlu mendiagnosis hasil atau menerjemahkan operasi tersebut ke model lain.

Sumber