Yanlış öğeyi değiştirmeden filtrelenmiş bir SwiftUI listesini düzenlemek için tek bir özgün dizi tutun ve görünür her satırı kararlı bir kimlik üzerinden bu diziye bağlayın. Bu macOS örneği, kendi uygulamanızda sentetik etiketleri filtrelemenize, yeniden adlandırmanıza, açıp kapatmanıza ve kaldırmanıza olanak tanır.

Tek başına filtreleme neden düzenlenebilir satırlar oluşturmaz?

Swift’te filter(_:), eşleşen öğeleri özgün sıralarıyla içeren yeni bir koleksiyon döndürür. Bu koleksiyonla kaynağı arasında yazılabilir bir ilişki oluşturmaz. Yapılardan oluşan bir dizide, ayrı depolanan filtrelenmiş dizideki bir öğeyi değiştirmek özgün diziyi güncellemez. Bu sonuçlar, filter(_:) işlevinin belgelenmiş davranışına ve Swift’in yapılar için değer semantiğine dayanır.

Örneğin, özgün dizide Grid, Tile ve Ring bulunduğunu, ancak filtrenin yalnızca Ring öğesini gösterdiğini varsayalım. Görünür konumu sıfır, özgün konumu ikidir. Görünür konumu özgün dizine yazmak için kullanırsanız Grid değişir. Görüntülenen sonuçları sıralamak da aynı uyumsuzluğa yol açabilir.

Bir bağlama, eksik olan okuma-yazma bağlantısını sağlar. Apple, Binding’i başka bir yerde sahip olunan verilere bağlantı olarak belgeler ve özel erişim için init(get:set:) sağlar. Burada her iki kapanım da çalıştığında özgün öğeyi kimliğine göre bulur. Hangi satırların görüneceğini filtrelenmiş koleksiyon belirler; düzenlemeler özgün diziye uygulanır. Binding ve init(get:set:) belgelerine bakın.

Düzenleyiciyi oluşturmadan önce kararlı kimliği seçin

Her öğenin adı, dahil edilme ayarı veya dizideki konumu değiştiğinde aynı kalan bir kimliği olmalıdır. Apple’ın Identifiable protokolü, kararlı varlık kimliğini tanımlar; ForEach ise oluşturduğu görünümleri ayırt etmek için tanımlanabilir verileri veya açık bir kimliği kullanır. Identifiable ve ForEach belgelerine bakın.

Örnek, her UUID’yi saklanan bir let id özelliğinde bir kez oluşturur. Bir etiketi yeniden adlandırmak kimliğini değiştirmez. Kimliği düzenlenebilir addan türetmekten veya kimlik her okunduğunda yeni bir UUID oluşturmaktan kaçının. Ayrıca farklı öğelerin kimliklerinin farklı olduğundan emin olun: arama bilerek eşleşen ilk kimliği bulur; bu nedenle yinelenen kimlikler yazma hedefini belirsiz kılar.

Veri akışını seçerken bu karar tablosunu kullanın:

Arayüz gereksinimi Bağlama yaklaşımı
Değişmeyen bir değer koleksiyonunu görüntüleme Salt okunur satırlara değerleri aktarın.
Özgün koleksiyonu doğrudan düzenleme Uygun olduğunda SwiftUI’nin koleksiyon bağlama desteğini kullanın.
Kaynaktakilerden farklı konumlarda bulunan sonuçları düzenleme Özgün öğeyi kararlı kimliğine göre bulun.
Öğe sonuçlardan çıktıktan sonra da düzenleyiciyi açık tutma Düzenleyiciye kendi seçimini ve yaşam süresi politikasını verin.

Devamı üçüncü durumu uygular. Ayrıca bir satır kaybolduğunda ne olacağını tanımlar; böylece eksik bir öğe hiçbir zaman denetlenmeyen bir dizi alt indisine dönüşmez.

Eksiksiz bir macOS örneği oluşturun

Xcode’da SwiftUI kullanan bir macOS uygulama projesi oluşturun ve bu örnek için dağıtım hedefini macOS 13 veya üzeri olarak ayarlayın. Oluşturulan uygulama ve içerik görünümü bildirimlerini aşağıdaki tek dosyayla değiştirin. Tam olarak bir @main giriş noktası bırakın; Apple’ın App belgeleri bu gereksinimi ve burada kullanılan WindowGroup yapısını açıklar.

Üst görünüm, özgün dizinin sahibi olarak özel @State kullanır; her alt görünüm bir @Binding alır. Bu, Apple’ın durumu alt görünümlerle paylaşma için belgelediği örüntüyü izler. Örnek yalnızca yerel, sentetik etiketler içerir ve verilerini bellekte tutar.

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)
        }
    }
}

Bu, mevcut bir görünümün içine yapıştırılacak bir kod parçası değil, eksiksiz bir uygulama örneğidir. Erişilen API belgelerine göre incelenmiştir; ancak bu ortamda Swift araç zinciri veya macOS SDK bulunmadığı için burada derlenmemiştir.

Bir düzenleme özgün diziye nasıl ulaşır?

Hesaplanan visibleItems özelliği, özgün diziden ve geçerli filtre ayarlarından üyeliği yeniden hesaplar. ForEach bu sonuçları alır ve eşleşen her öğe için bir satır oluşturur. Satırları oluşturmadan önce filtreleme, her yinelemenin içine koşullu sıfır veya bir satır dalı koymayı da önler; bu örüntü Apple’ın ForEach performans rehberinde ele alınır.

Bir satır bağlamasını okuduğunda alıcı, geçerli özgün dizide kendi kimliğini arar. Anlık görüntü, yalnızca öğe zaten kaldırılmışsa yedek değer olarak kullanılır. Öğe mevcutken alıcı, satır oluşturulduğu sırada yakalanan anlık görüntü yerine güncel özgün değeri döndürür.

Kullanıcı Label name alanını düzenlediğinde $item.name, öğe bağlamasını yazılabilir ad özelliğine yansıtır. Apple, bu özellik yansıtmasını Binding’in dynamic-member alt indisi üzerinden belgeler. Ardından ayarlayıcı öğenin geçerli özgün dizinini bulur ve saklanan kimliği koruyarak düzenlenebilir alanlarını günceller.

Arama, isteğe bağlı bir dizin döndüren ve eşleşme olmadığında nil döndüren firstIndex(where:) işlevini kullanır. Kod, alt indis kullanmadan önce bu sonucu denetler. Her yazma işleminde aramayı yinelediği için özgün diziyi ters çevirmek, ayarlayıcının eski bir dizini kullanmasına yol açmaz. Apple’ın firstIndex(where:) belgelerine bakın.

Bir satır kaybolduğunda ne olur?

Birbirinden farklı iki durum vardır. Bir düzenleme bir öğenin filtreye uymamasına yol açarsa öğe özgün dizide varlığını sürdürür. Düzenleme korunur, ancak satır sonuçlardan çıkar. Örneğin, Included only etkinken Included seçimini kapatmak satırı gizler; filtreyi yeniden kapatmak satırı tekrar gösterir.

Öğe özgün diziden kaldırılmışsa eski bir bağlama üzerinden yapılan sonraki yazma reddedilir. Örnek, Düzenleme atlandı: etiket kaldırılmış. iletisini gösterir. Anlık görüntüyü yeniden diziye eklemez. Alıcıdan anlık görüntü döndürmek ve eksik öğelere yazmayı reddetmek, bu örneğin açıkça belirlediği politikalardır; SwiftUI’nin sağladığı garantiler değildir.

Canlı ad filtrelemesi de bir etkileşim kararı gerektirir. Apple, bir dizeye bağlı TextField’ın düzenleme sırasında değerini sürekli güncellediğini belgeler. Bu nedenle yeni adı sorguyla eşleşmemeye başladığı anda satır kaybolabilir. Kesintisiz yeniden adlandırma için önce sorguyu temizleyin veya taslağı açık bir eylemle kaydeden ayrı bir düzenleyici tasarlayın. TextField belgelerine bakın.

Kısa bir masaüstü kontrolüyle davranışı doğrulayın

Aşağıdaki kontroller, örnekten çıkarılan beklenen davranışları açıklar; yürütme sonuçları olarak sunulmamıştır. Başlangıç etiketleri için yeni bir çalıştırma başlatın ve adımları sırayla uygulayın.

Eylem Beklenen sonuç
Filter labels alanına Ring yazın. Yalnızca Ring görünür.
Adını Ring Two olarak değiştirin. Özgün dizi gösterimi Grid · Tile · Ring Two olur.
Sorguyu temizleyin ve Reverse original order düğmesine tıklayın. Gösterim Ring Two · Tile · Grid olur.
Grid adını Grid Two olarak değiştirin. Yalnızca o öğenin özgün değeri değişir.
Included only seçeneğini etkinleştirin, ardından Grid Two için Included seçimini kapatın. Satır kaybolur, ancak adı özgün dizi gösteriminde kalır.
Included only seçeneğini devre dışı bırakın. Grid Two, anahtarı kapalı olarak geri gelir.
Tile yazın, ardından Remove düğmesine tıklayın. Hem satır hem de özgün dizideki girişi kaybolur.

Yanlış öğe değişiyorsa görüntülenen bir konumun özgün dizi indisi olarak kullanılıp kullanılmadığını inceleyin. Filtre temizlendikten sonra düzenlemeler kayboluyorsa satırın ayrı bir @State kopyasına sahip olup olmadığını kontrol edin. Yeniden adlandırılan bir satır yeni bir öğe gibi davranıyorsa kimliğinin düzenlenebilir içeriğe bağlı olup olmadığını inceleyin. Bu kontroller, örneğin açık tuttuğu üç ilişkiyi hedefler: kaynak sahipliği, yazma hedefi ve satır kimliği.

Çözümü macOS ve API sınırları içinde tutun

Kısa, düzenlenebilir etiketler için örnekteki SwiftUI TextField gerekli arayüzü sağlar. Daha kapsamlı bir belge düzenleyici, bunun yerine zengin metni destekleyen ve NSTextStorage ile NSTextLayoutManager gibi metin sistemi bileşenleriyle bütünleşen AppKit NSTextView’a ihtiyaç duyabilir. Bu, düzenleme denetimini değiştirir; uygulamanın her düzenlemenin hangi özgün model öğesine uygulanacağına yine karar vermesi gerekir. NSTextView belgelerine bakın.

SwiftUI hiyerarşisindeki bir AppKit görünümü için desteklenen köprü NSViewRepresentable’dır. Apple, temsil edilen görünümün oluşturulup güncellenmesini ve etkileşimleri iletmek için bir koordinatör kullanılmasını belgeler. Gelecekte özel bir düzenleyici oluştururken aynı kararlı kimlikli yazma hedefini koruyun. Temsil edilen görünümün düzenini doğrudan yönetilen çerçevesini veya sınırlarını değiştirerek değil, SwiftUI’nin yönetmesini sağlayın. NSViewRepresentable belgelerine bakın.

AppKit uzantılarının TextKit sınırları da vardır: Apple, NSTextView.layoutManager öğesine erişmenin görünümü NSLayoutManager kullanan uyumluluk moduna geçirebileceğini belgeler. Bu API’leri benimsemeden önce kullandığınız belirli metin API’lerinin kullanılabilirliğini ve davranışını kontrol edin. Koleksiyon bağlama çözümü tek başına TextKit özelleştirmesi gerektirmez. NSTextView içindeki uyumluluk açıklamasına bakın.

Daha büyük koleksiyonlarda arama maliyetini hesaba katın. Apple, hem filter(_:) hem de firstIndex(where:) için doğrusal karmaşıklık belgeler; bu nedenle her satır için yinelenen aramalar koleksiyon büyüdükçe kayda değer hâle gelebilir. Kimlikten dizine eşleme önbelleği eklemeden önce arayüzünüzü ölçün ve yapısal değişiklikler dizinleri değiştirdiğinde böyle bir önbelleği yeniden oluşturun. filter(_:) ve firstIndex(where:) belgelerine bakın.

Bu hesaplanan bağlamalara yönelik UI erişimini, örnekte olduğu gibi ana aktörde tutun. Apple’ın bağlama başlatıcısı belgeleri, hesaplanan bir bağlamaya erişim izninin kapanımlarının yalıtımına bağlı olduğunu açıklar. Bir bağlamayı başka bir yere aktarmak bu kısıtlamaları ortadan kaldırmaz. init(get:set:) belgelerine bakın.

Bu filtrelenmiş etiket düzenleyicisi için tek bir özgün dizi tutun, satırlarda kararlı kimlikler kullanın ve yazma hedefini düzenleme anında bulun. Ardından kaybolma davranışına bilinçli olarak karar verin: filtre dışına çıkan bir öğe düzenlemesini korur; kaldırılan bir öğeye yönelik sonraki yazmalar ise kullanıcıya görünür bir açıklamayla reddedilir.

Kaynaklar