Untuk mengedit senarai SwiftUI yang ditapis tanpa mengubah item yang salah, kekalkan satu tatasusunan asal dan pautkan setiap baris yang kelihatan kembali kepadanya menggunakan ID yang stabil. Contoh macOS ini membolehkan anda menapis, menamakan semula, menogol dan membuang label sintetik dalam aplikasi anda sendiri.

Mengapa penapisan sahaja tidak menghasilkan baris yang boleh diedit

filter(_:) dalam Swift memulangkan koleksi baharu yang mengandungi elemen yang sepadan dalam susunan asalnya. Ia tidak mewujudkan hubungan boleh tulis antara koleksi itu dengan sumbernya. Bagi tatasusunan struktur, mengubah elemen dalam tatasusunan ditapis yang disimpan secara berasingan tidak mengemas kini tatasusunan asal. Kesimpulan ini berdasarkan tingkah laku yang didokumenkan bagi filter(_:) dan semantik nilai untuk struktur dalam Swift.

Sebagai contoh, katakan tatasusunan asal mengandungi Grid, Tile dan Ring, tetapi penapis hanya memaparkan Ring. Kedudukan yang kelihatan ialah sifar; kedudukan asalnya ialah dua. Menggunakan kedudukan yang kelihatan untuk menulis ke dalam tatasusunan asal akan mengubah Grid. Mengisih hasil yang dipaparkan boleh menghasilkan ketidakpadanan yang sama.

Binding menyediakan sambungan baca dan tulis yang tiada itu. Apple mendokumenkan Binding sebagai sambungan kepada data yang dimiliki di tempat lain, serta menyediakan init(get:set:) untuk akses tersuai. Di sini, kedua-dua closure mencari item asal mengikut ID setiap kali dijalankan. Koleksi yang ditapis menentukan baris yang dipaparkan; tatasusunan asal menerima suntingan. Lihat Binding dan init(get:set:).

Pilih identiti stabil sebelum membina editor

Setiap item memerlukan ID yang kekal sama apabila namanya, tetapan kemasukannya atau kedudukan tatasusunannya berubah. Protokol Identifiable Apple menerangkan identiti entiti yang stabil, manakala ForEach menggunakan data yang boleh dikenal pasti atau ID yang dinyatakan secara jelas untuk membezakan paparan yang dijananya. Lihat Identifiable dan ForEach.

Contoh ini mencipta setiap UUID sekali dalam sifat let id yang disimpan. Menamakan semula label tidak mengubah ID-nya. Elakkan daripada menerbitkan identiti daripada nama yang boleh diedit atau menjana UUID baharu setiap kali ID dibaca. Pastikan juga item yang berlainan mempunyai ID yang berlainan: carian ini sengaja mencari ID sepadan yang pertama, jadi ID pendua akan menjadikan sasaran tulis tidak jelas.

Gunakan panduan keputusan ini apabila memilih aliran data:

Keperluan antara muka Pendekatan binding
Paparkan koleksi nilai tanpa perubahan Serahkan nilai kepada baris baca sahaja.
Edit koleksi asal secara langsung Gunakan sokongan binding koleksi SwiftUI jika sesuai.
Edit hasil yang kedudukannya berbeza daripada sumber Cari item asal menggunakan ID stabil.
Kekalkan editor terbuka selepas itemnya tiada lagi dalam hasil Berikan editor pilihan dan dasar kitar hayatnya sendiri.

Bahagian seterusnya melaksanakan kes ketiga. Ia juga mentakrifkan perkara yang berlaku apabila baris hilang, supaya item yang tiada tidak pernah menyebabkan subskrip tatasusunan tanpa semakan.

Bina contoh macOS yang lengkap

Cipta projek aplikasi macOS dalam Xcode menggunakan SwiftUI, dan tetapkan sasaran penggunaan kepada macOS 13 atau lebih baharu untuk contoh ini. Gantikan deklarasi aplikasi dan paparan kandungan yang dijana dengan satu fail berikut. Kekalkan tepat satu titik masuk @main; dokumentasi App Apple menerangkan keperluan itu serta struktur WindowGroup yang digunakan di sini.

Ibu bapa memiliki tatasusunan asal dalam @State peribadi; setiap anak menerima @Binding. Ini mengikuti corak Apple yang didokumenkan untuk berkongsi state dengan subpaparan. Contoh ini hanya mengandungi label sintetik setempat dan menyimpan datanya dalam memori.

import SwiftUI
import Foundation

private struct LabelItem: Identifiable, Sendable {
    let id = UUID()
    var name: String
    var isIncluded: Bool
}

@main
struct FilteredLabelsApp: App {
    var body: some Scene {
        WindowGroup {
            LabelEditor()
        }
    }
}

@MainActor
private struct LabelEditor: View {
    @State private var items = [
        LabelItem(name: "Grid", isIncluded: true),
        LabelItem(name: "Tile", isIncluded: false),
        LabelItem(name: "Ring", isIncluded: true)
    ]
    @State private var query = ""
    @State private var includedOnly = false
    @State private var notice = ""

    private var visibleItems: [LabelItem] {
        items.filter { item in
            (!includedOnly || item.isIncluded) &&
            (query.isEmpty ||
             item.name.localizedCaseInsensitiveContains(query))
        }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            TextField("Filter labels", text: $query)
            Toggle("Included only", isOn: $includedOnly)

            Button("Reverse original order") {
                items.reverse()
            }

            List {
                ForEach(visibleItems) { snapshot in
                    LabelRow(item: itemBinding(for: snapshot)) {
                        items.removeAll { $0.id == snapshot.id }
                    }
                }
            }

            Text("Original array:")
                .font(.headline)
            Text(items.map(\.name).joined(separator: " · "))
            Text(notice)
        }
        .padding()
        .frame(minWidth: 540, minHeight: 360)
    }

    private func itemBinding(
        for snapshot: LabelItem
    ) -> Binding<LabelItem> {
        let id = snapshot.id

        return Binding(
            get: {
                items.first { $0.id == id } ?? snapshot
            },
            set: { updated in
                guard let index = items.firstIndex(where: {
                    $0.id == id
                }) else {
                    notice = "Edit skipped: label was removed."
                    return
                }

                items[index].name = updated.name
                items[index].isIncluded = updated.isIncluded
                notice = ""
            }
        )
    }
}

@MainActor
private struct LabelRow: View {
    @Binding var item: LabelItem
    let remove: () -> Void

    var body: some View {
        HStack {
            TextField("Label name", text: $item.name)
            Toggle("Included", isOn: $item.isIncluded)
            Button("Remove", action: remove)
        }
    }
}

Ini contoh aplikasi yang lengkap, bukannya cebisan kod untuk ditampal di dalam paparan sedia ada. Kod ini telah disemak berdasarkan dokumentasi API yang diperoleh, tetapi tidak dikompil di sini kerana persekitaran ini tidak mempunyai toolchain Swift atau SDK macOS.

Cara suntingan sampai ke tatasusunan asal

Sifat visibleItems yang dikira mengira semula keanggotaan berdasarkan tatasusunan asal dan tetapan penapis semasa. ForEach menerima hasil itu lalu mencipta satu baris bagi setiap item yang sepadan. Menapis sebelum membina baris juga mengelakkan cabang bersyarat yang menghasilkan sifar atau satu baris di dalam setiap lelaran, corak yang dibincangkan dalam panduan prestasi ForEach Apple.

Apabila baris membaca binding-nya, getter mencari ID-nya dalam tatasusunan asal semasa. Snapshot hanya menjadi nilai sandaran bagi item yang sudah dibuang. Selagi item itu wujud, getter memulangkan nilai asal semasa, bukannya snapshot yang dirakam ketika baris dicipta.

Apabila pengguna mengedit Label name, $item.name memproyeksikan binding item kepada sifat namanya yang boleh ditulis. Apple mendokumenkan unjuran sifat ini melalui subskrip dynamic-member milik Binding. Setter kemudian mencari indeks asal semasa item itu dan mengemas kini medan yang boleh diedit, sambil mengekalkan ID yang disimpan.

Carian menggunakan firstIndex(where:), yang memulangkan indeks pilihan dan memulangkan nil jika tiada padanan. Kod menyemak hasil itu sebelum menggunakan subskrip. Oleh sebab carian diulang bagi setiap operasi tulis, membalikkan susunan tatasusunan asal tidak menyebabkan setter menggunakan indeks lama. Lihat dokumentasi firstIndex(where:) Apple.

Apa yang berlaku apabila baris hilang?

Terdapat dua keadaan yang berbeza. Jika suntingan menyebabkan item tidak lagi memenuhi penapis, item itu masih wujud dalam tatasusunan asal. Suntingan dikekalkan, tetapi barisnya tidak lagi muncul dalam hasil. Contohnya, mematikan Included ketika Included only diaktifkan akan menyembunyikan baris itu; mematikan penapis akan memaparkannya semula.

Jika item itu telah dibuang daripada tatasusunan asal, operasi tulis kemudian melalui binding lama akan ditolak. Contoh ini melaporkan Edit skipped: label was removed. Ia tidak memasukkan semula snapshot ke dalam tatasusunan. Memulangkan snapshot daripada getter dan menolak operasi tulis untuk item yang tiada ialah dasar yang ditetapkan secara jelas oleh contoh ini, bukannya jaminan yang diberikan oleh SwiftUI.

Penapisan nama secara langsung juga memerlukan keputusan interaksi. Apple mendokumenkan bahawa TextField yang terikat kepada rentetan mengemas kini nilainya secara berterusan semasa penyuntingan. Akibatnya, sesuatu baris boleh hilang sebaik sahaja nama baharunya tidak lagi sepadan dengan pertanyaan. Untuk menamakan semula tanpa gangguan, kosongkan pertanyaan dahulu atau reka editor berasingan yang mengesahkan draf melalui tindakan yang jelas. Lihat TextField.

Sahkan tingkah laku dengan semakan ringkas pada desktop

Semakan berikut menerangkan tingkah laku yang dijangka berdasarkan contoh ini; ini bukan hasil pelaksanaan yang dilaporkan. Mulakan sesi baharu untuk mendapatkan label awal, kemudian lakukan langkah-langkah mengikut urutan.

Tindakan Hasil yang dijangka
Masukkan Ring dalam Filter labels. Hanya Ring dipaparkan.
Namakan semula kepada Ring Two. Paparan tatasusunan asal menjadi Grid · Tile · Ring Two.
Kosongkan pertanyaan dan klik Reverse original order. Paparan menjadi Ring Two · Tile · Grid.
Namakan semula Grid kepada Grid Two. Hanya nilai asal item itu berubah.
Aktifkan Included only, kemudian matikan Included untuk Grid Two. Baris itu hilang, tetapi namanya kekal dalam paparan tatasusunan asal.
Matikan Included only. Grid Two muncul semula dengan togolnya dimatikan.
Masukkan Tile, kemudian klik butang Remove padanya. Baris itu dan entri tatasusunan asalnya kedua-duanya hilang.

Jika item yang salah berubah, periksa sama ada ofset yang dipaparkan digunakan sebagai indeks tatasusunan asal. Jika suntingan hilang selepas penapis dikosongkan, periksa sama ada baris memiliki salinan @State yang berasingan. Jika baris yang dinamakan semula berkelakuan seperti item baharu, periksa sama ada ID-nya bergantung pada kandungan yang boleh diedit. Semakan ini menyasarkan tiga hubungan yang dikekalkan secara jelas oleh contoh ini: pemilikan sumber, destinasi tulis dan identiti baris.

Kekalkan penyelesaian dalam had macOS dan API-nya

Untuk label pendek yang boleh diedit, TextField SwiftUI dalam contoh ini menyediakan antara muka yang diperlukan. Editor dokumen yang lebih lengkap mungkin memerlukan NSTextView AppKit, yang menyokong teks berformat kaya dan bersepadu dengan komponen sistem teks seperti NSTextStorage dan NSTextLayoutManager. Ini mengubah kawalan penyuntingan; aplikasi tetap perlu menentukan item model asal yang menerima setiap suntingan. Lihat NSTextView.

Jambatan yang disokong untuk paparan AppKit dalam hierarki SwiftUI ialah NSViewRepresentable. Apple mendokumenkan cara mencipta dan mengemas kini paparan yang diwakilkan serta menggunakan koordinator untuk meneruskan interaksi. Untuk editor tersuai pada masa hadapan, kekalkan destinasi tulis berdasarkan ID stabil yang sama. Biarkan SwiftUI mengurus reka letak paparan yang diwakilkan, dan jangan ubah bingkai atau sempadan yang diuruskannya secara langsung. Lihat NSViewRepresentable.

Sambungan AppKit juga mempunyai batas TextKit: Apple mendokumenkan bahawa mengakses NSTextView.layoutManager boleh menukar paparan kepada mod keserasian yang menggunakan NSLayoutManager. Semak ketersediaan dan tingkah laku API teks tertentu sebelum menggunakannya. Penyelesaian binding koleksi itu sendiri tidak memerlukan penyesuaian TextKit. Lihat perbincangan keserasian dalam NSTextView.

Bagi koleksi yang lebih besar, ambil kira kerja carian. Apple mendokumenkan kerumitan linear bagi kedua-dua filter(_:) dan firstIndex(where:); oleh itu, carian berulang bagi setiap baris boleh menjadi ketara apabila koleksi membesar. Ukur antara muka anda sebelum memperkenalkan cache ID kepada indeks, dan bina semula cache sedemikian setiap kali perubahan struktur mengubah indeks. Lihat filter(_:) dan firstIndex(where:).

Kekalkan akses UI kepada binding yang dikira ini pada aktor utama, seperti dalam contoh. Dokumentasi initializer binding Apple menerangkan bahawa akses yang dibenarkan bagi binding yang dikira bergantung pada pengasingan closure-nya. Menghantar binding ke tempat lain tidak menghapuskan sekatan tersebut. Lihat init(get:set:).

Bagi editor label bertapis ini, kekalkan satu tatasusunan asal, gunakan ID stabil untuk baris dan cari sasaran tulis ketika suntingan dibuat. Kemudian tentukan secara sengaja tingkah laku apabila baris hilang: item yang ditapis keluar mengekalkan suntingannya, manakala item yang dibuang menolak operasi tulis seterusnya dengan penjelasan yang kelihatan.

Sumber