Bir SwiftUI listesini düzenlerken imlecin sabit kalması için her öğeye değişmez bir kimlik verin, metnine doğrudan bağlayın ve düzenleyicinin görünüm hiyerarşisindeki yerini koruyun. Uygulamanızın ekleme noktasını da denetlemesi gerekiyorsa macOS 15 veya sonraki sürümlerde bir seçim bağlaması kullanın.
Bu kılavuzda Blue tile gibi sentetik etiketleri düzenlemek için küçük bir Mac uygulaması oluşturuluyor. Amaç, metni ve çevresindeki liste güncellenirken aynı öğede yazmaya devam etmektir. Ayrıca seçimi geri yüklemek için ne zaman ek duruma veya bir AppKit düzenleyicisine ihtiyaç duyulabileceği açıklanıyor.
Bir liste öğesi değiştiğinde imleç neden hareket eder?
Önce öğenin kimliğinin metniyle birlikte değişip değişmediğini kontrol edin. SwiftUI, ardışık görünüm değerlerini ilişkilendirmek için kimliği kullanır ve kalıcı görünüm durumu bu kimliğe bağlıdır. Bir görünümün body değerinin yeniden değerlendirilmesi normaldir; kimliğinin değiştirilmesi ise yaşam süresini sona erdirir. Apple bu ilişkiyi Demystify SwiftUI içinde açıklar.
Düzenlenebilir bir koleksiyonda satırı, düzenlenebilir etiketiyle tanımlamayın. id: \.self ile bir dizenin kimliği, dize değiştiğinde değişir. Hash değeri etiketini içeren bir modelde de aynı sorun yaşanabilir. Taşınabilir öğeleri dizi konumuyla tanımlamak da kimliği mantıksal öğeye değil bir yuvaya bağlar.
Bunun yerine, düzenlemeler boyunca değişmeyen saklanan bir tanımlayıcı kullanın. Apple'ın ForEach belgeleri, tanımlanabilir öğelerden oluşan koleksiyonları veya açık bir tanımlayıcı anahtar yolunu destekler. Bu düzenleyici için uygun seçenek, öğeye ait saklanan bir UUID değeridir.
Ayrıca düzenleyiciden ve üst öğelerinden .id(item.text) veya .id(UUID()) gibi değiştiricileri kaldırın. Apple, id(_:) öğesine geçirilen değerin değiştirilmesinin, durumu da dahil olmak üzere görünüm kimliğini sıfırladığını açıkça belgeliyor. Kararlı bir satır kimliği, her düzenlemede kimliği değişen bir üst öğeyi telafi edemez.
Kimliği, odağı ve seçimi ayrı tutun
Bu üç durum parçası farklı sorulara yanıt verir. Kimlik, güncellemeler boyunca hangi mantıksal satırın devam ettiğini belirler. Odak, klavye girişini hangi denetimin aldığını belirler. Seçim ise bu denetimin içindeki ekleme noktasını veya seçili metni belirler.
FocusState, uygulamanın odağı gözlemlemesini ve değiştirmesini sağlar. Her alana, örneğin öğe kimliği gibi ayrı bir odak değeri verin. Her satırı aynı Boolean değerine bağlamak, hangi tekil öğenin programlı odak isteğini alacağını belirleyemez.
TextSelection, macOS'te bir ekleme noktasını, seçili aralığı veya birden fazla seçimi temsil eder. Seçim bağlamalı TextField başlatıcısı macOS 15'ten itibaren kullanılabilir.
Aşağıdaki uygulama, belgelenmiş bu rolleri izler: model kimliği ve metni tutar, liste odaklanmış öğe kimliğini tutar ve her canlı satır kendi seçimini tutar. Bu tasarım, satır ekranda kaldığı sürece kesintisiz düzenleme içindir; satır kaldırılıp yeniden oluşturulduktan sonra her düzenleme ayrıntısının geri yükleneceğini garanti etmez.
Kararlı öğe kimliğine sahip bir liste oluşturun
Xcode'da bir macOS SwiftUI uygulaması oluşturun ve dağıtım hedefini macOS 15 veya sonraki bir sürüm olarak ayarlayın. Seçim bağlamalı başlatıcıyı içeren bir SDK kullanın. Hedefte yalnızca bir @main bildirimi bırakarak oluşturulan uygulama ve içerik görünümü bildirimlerini bu eksiksiz örnekle değiştirin.
import Foundation
import SwiftUI
struct LabelItem: Identifiable {
let id: UUID
var text: String
init(id: UUID = UUID(), text: String) {
self.id = id
self.text = text
}
}
@main
struct CaretListApp: App {
var body: some Scene {
WindowGroup {
LabelListView()
}
}
}
struct LabelListView: View {
@State private var items = [
LabelItem(text: "Blue tile"),
LabelItem(text: "Round pebble"),
LabelItem(text: "Paper square")
]
@FocusState private var focusedID: UUID?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Button("Add label") {
items.append(LabelItem(text: "New label"))
}
.keyboardShortcut("n", modifiers: [.command, .shift])
Button("Reverse rows") {
items.reverse()
}
.keyboardShortcut("r", modifiers: [.command, .shift])
}
List {
ForEach($items) { $item in
LabelRow(item: $item, focus: $focusedID)
}
}
}
.padding()
.frame(minWidth: 420, minHeight: 260)
}
}
struct LabelRow: View {
@Binding var item: LabelItem
let focus: FocusState<UUID?>.Binding
@State private var selection: TextSelection? = nil
var body: some View {
TextField("Label", text: $item.text, selection: $selection)
.textFieldStyle(.roundedBorder)
.focused(focus, equals: item.id)
}
}
Saklanan id, bir öğe oluşturulurken oluşturulur. text değerini değiştirmek veya mevcut diziyi tersine çevirmek bu kimliği korur. Bağlama dayalı ForEach, her satıra düzenlenebilir model değerini sağlar ve alan, öğenin metnine doğrudan yazar.
Dizeye bağlanan bir TextField, düzenleme sırasında bağlamasını sürekli günceller. Bu nedenle bu örnekte, her tuş vuruşunu ikinci bir diziye kopyalamak veya listeyi yeniden oluşturmak için ek bir değişiklik işleyicisi gerekmez.
Satırda, konumu tutarlı olan tek bir düzenleyici bulunur. Metne bağlı kimlik değiştiricisi, otomatik seçim sıfırlaması veya her güncellemede odak isteği yoktur. Seçim bağlaması, uygulama kodunun seçimi tekrar tekrar üzerine yazmasına gerek kalmadan alanın mevcut seçimini iletmesini sağlar.
Örnek, alınan API bildirimlerine göre incelenmiştir. Bu ortamda Swift derleyicisi veya macOS çalışma zamanı bulunmadığından derlenmemiş ya da çalıştırılmamıştır.
Modeli yenilerken kimlikleri koruyun
Yenileme sırasında, aynı etiketleri temsil etmeye devam eden öğelerin tanımlayıcıları korunmalıdır. Mevcut her öğe için yeni bir LabelItem(text:) oluşturmak, görünen metin değişmese bile yeni kimlikler üretir.
Uygulamanızın kendi saklanan kayıtlarından bir koleksiyonu yeniden oluştururken, her kaydın mevcut kimliğini yeni model değerine aktarın. Yalnızca gerçekten yeni bir öğe oluştururken yeni kimlik üretin. Kayıtların kalıcı bir tanımlayıcısı yoksa, bunu düzenlenebilir metinden türetmek yerine öğe oluşturulurken ekleyin.
Anında yer değiştirmesi özellikle istenmiyorsa her tuş vuruşundan sonra etikete göre sıralama yapmayın. Kararlı kimlik, SwiftUI'ye hangi öğenin taşındığını söyler; ancak biri yazarken satırın konum değiştirmesi düzenleme deneyimini yine de etkiler. Açık bir sıralama eylemi arayüzde daha belirgin bir geçiş sağlar.
İmleç hâlâ sıçrıyorsa sorunu işe yarar bir sırayla teşhis edin
Aşağıdaki karar rehberi, kimlik, odak ve seçimin belgelenmiş ayrımından çıkarılmıştır. Geri yükleme kodu eklemeden önce sorunun kaynağını daraltmaya yardımcı olur.
| Gözlenen davranış | Önce kontrol edilecekler | Uygun sonraki adım |
|---|---|---|
| Düzenleyici her karakterden sonra sıfırlanıyor | Metinden türetilen kimlikler; değişen .id(...) değerleri |
Öğe kimliğini ve üst öğelerin kimliklerini kararlı tutun |
| Klavye girişi alana ulaşmıyor | Odak değişiklikleri veya düzenleyicinin kaldırılması | focusedID ve koşullu görünüm dallarını inceleyin |
| Odak alanda kalıyor ama imleç hareket ediyor | Uygulamanın metne veya seçime yazması | Koşulsuz yeniden yazmaları ve seçim atamalarını kaldırın |
| Yeniden sıralama sonrasında düzenleme durumu bir konumu izliyor | Dizin tabanlı kimlik | Mantıksal öğeyi saklanan kimliğiyle tanımlayın |
| Satır filtrelenip çıkarıldıktan ve geri eklendikten sonra seçim kayboluyor | Satıra özgü durumun yaşam süresi | Geri yükleme bilgisini kaldırılabilir satırın üstünde saklayın |
Önce sorunu sıralama, filtreleme, biçimlendirme veya model yenilemesi olmadan yeniden oluşturun. Blue tile metninin ortasında düzenleme yapın. Sorun yoksa işlemleri teker teker yeniden ekleyin. Böylece sıçramaya yol açan değişikliği yalıtabilirsiniz.
Ardından yapısal değişiklikleri inceleyin. Ayrı düzenleyiciler içeren iki koşullu dal arasında geçiş yapmak, düzenleyicinin kimliğini değiştirebilir. Apple'ın kimlik ve yaşam süresi açıklaması, yalnızca görünümünün değişmesi gerektiğinde tek bir düzenleyiciyi ekranda tutmayı destekler.
Son olarak metin bağlamasına yapılan yazmaları inceleyin. Düzenleme sırasında dizeyi kırpan, büyük harfe dönüştüren veya başka şekilde değiştiren bir işleyici, ikinci bir düzenleme işlemi başlatır. Etiketleri temizlemek için pratik bir yöntem, yazma sırasında taslağı değiştirmemek ve dönüşümü gönderim sırasında uygulamaktır.
Satır kaldırıldığında nelerin korunması gerektiğine karar verin
Satıra özgü seçim durumu, yaşamaya devam eden bir satır için uygundur. Arayüz satırı kaldırdığında tek başına yeterli bir geri yükleme kaynağı değildir. Apple'ın macOS 15 sürüm notları ayrıca ForEach'in kullanılmayan alt görünümlere ait kalıcı durumu geri alabileceğini belirtir.
Bir öğeye geri dönüldüğünde düzenleme konumu geri yüklenmeliyse, geri yükleme bilgisini kaldırılabilir satırların üstündeki bir sahipte, öğe kimliğine göre anahtarlayarak saklayın. Bu bilgiyi tanımladığı metin sürümüyle ilişkilendirin. Öğe kalıcı olarak silindiğinde bu bilgiyi kaldırın.
Odağı geri yüklemek ve seçimi geri yüklemek ayrı işlemlerdir. Öğe artık yoksa, kaydedilen imleci aynı dizi konumunu alan başka bir öğeye yönlendirmeyin.
Programlı metin değişikliklerini açık bir imleç politikasıyla ele alın
Kararlı kimlik, uygulamanız metni değiştirdikten sonra imlecin nereye gitmesi gerektiğine karar veremez. Bu, eyleme uygun bir kural gerektirir: mantıksal konumunu korumak, yeni metni seçmek veya sona gitmek.
SwiftUI'daki TextSelection başlatıcıları, String.Index tabanlı bir ekleme noktasını veya aralıkları kabul eder. Uygulamanız metni değiştirdiğinde, ortaya çıkan dizeden yeni bir seçim oluşturun. Eski seçimi kalıcı bir sayısal yer işareti olarak değerlendirmeyin.
Swift dizeleri String.Index kullanır ve karakter sayıları NSString tarafından kullanılan UTF-16 uzunluğundan farklı olabilir. Bu ayrımlar Dizeler ve Karakterler bölümünde belgelenmiştir. SwiftUI seçimleriyle Cocoa metin aralıkları arasında dönüşüm yaparken bu ayrımlar önemlidir.
Örnek bir ekleme kuralı düşünelim: metin Blue tile olsun ve imleç Blue sözcüğünün hemen ardından bulunsun. Amaç, imlecin özgün metne göre konumunu korumaksa başa Soft eklemek imleci beş konum ileri taşımalıdır. Sayısal ofsetini değiştirmemek, onu etiketin başka bir bölümüne yerleştirir.
Bu örnekte yalnızca basit ASCII metni kullanılıyor. Genel dizeler için ofset birimini tanımlayın ve ortaya çıkan sınırları doğrulayın. Değiştirilen metin seçili metinle çakışıyorsa, sonucu bilinçli olarak belirlemek gerekir; ofseti yeni uzunluğa kırpmak, amaçlanan seçimi belirlemez.
AppKit veya TextKit ne zaman kullanılmalı?
Daha eski macOS dağıtım hedeflerinde macOS 15 SwiftUI seçim bağlamalı başlatıcısı kullanılamaz. Düz bir SwiftUI alanındaki kararsız kimliği yine de düzeltebilirsiniz. Uygulamanın ayrıca açık seçim denetimine ihtiyacı varsa desteklenen bir tümleştirme yolu, NSViewRepresentable ile sarmalanmış bir AppKit düzenleyicisidir.
Apple, NSTextView'i TextKit yeteneklerini bir araya getiren bir düzenleme görünümü olarak belgeler. Yalnızca imleci korumak için özel bir yerleşim motoru oluşturmanız gerekmez. Düzenleme durumunu doğrudan denetlemeniz gerektiğinde özel bir metin görünümü seçin.
Bir sarmalayıcı şu güncelleme sırasını izlemelidir:
- AppKit düzenleyicisini
makeNSView(context:)içinde oluşturun ve eşgüdümleyici aracılığıyla temsilciyi bağlayın. - Kullanıcı düzenlemelerini bağlı modele, seçim değişikliklerini ise öğe kimliğiyle ilişkilendirilmiş duruma iletin.
updateNSViewiçinde, herhangi bir atama yapmadan önce model metnini düzenleyicinin mevcut metniyle karşılaştırın.- Gerçek bir programlı değiştirme için, ortaya çıkan metne göre değiştirme seçimini hesaplayıp bir kez uygulayın.
- Bu uygulama güncellemesinin neden olduğu temsilci geri bildirimini bastırın ve ertelenmiş dönüşümleri uygulamadan önce işaretli metin bileşimini ele alın.
Bunlar, ikinci bir tam kod örneği değil, köprü için uygulama kurallarıdır. Apple, metin görünümü seçimlerini okumak ve yazmak için selectedRanges öğesini, seçim değişikliklerini almak için ise textViewDidChangeSelection(_:) öğesini sağlar. Düzenleyici destekliyorsa birden çok aralığı koruyun.
Bileşime duyarlı güncellemeler için hasMarkedText(), işaretli metin bulunup bulunmadığını bildirir. Pratik bir yaklaşım, bileşim sırasında zorunlu olmayan uygulama dönüşümlerini ertelemek ve bileşim sona erdikten sonra uygulamaktır.
AppKit NSTextField'ın dikkate alınması gereken başka bir yaşam döngüsü özelliği vardır: Apple, basit denetimlerin pencerenin alan düzenleyicisini paylaştığını belgeler. Kaydedilen seçimi, paylaşılan bu düzenleyiciyi kalıcı olarak satıra özgü bir nesne saymak yerine öğe ve etkin düzenleme oturumuyla ilişkilendirin.
Sarmalayıcıyı belgelenmiş tümleştirme sınırları içinde tutun. SwiftUI, temsil edilen görünümün yerleşimini denetler ve Apple, görünümün yönetilen çerçevesine veya sınırlarına doğrudan değer atanmaması konusunda uyarır. Ayrıca SwiftUI'ın dahili liste uygulamasıyla ilgili varsayımlardan kaçının: macOS 15 sürüm notları, anahat içermeyen listelerin artık NSTableView kullanmadığını belirtir.
Düzenleme sonucunu kendi pencerenizde doğrulayın
Her davranışı ayrı ayrı kontrol etmek için örnek etiketleri kullanın. Aşağıdakiler doğrulama adımlarıdır; bildirilmiş test sonuçları değildir.
- İmleci
Blueile onu izleyen boşluk arasına yerleştirin.smallyazın. Yazmanın beklenen ekleme noktasında devam ettiğini doğrulayın. tileöğesini seçipsquareile değiştirin. Değiştirmenin yalnızca seçili metni etkilediğini doğrulayın.- Düzenleme sırasında
Command-Shift-NileAdd labelkomutunu çalıştırın. Etkin öğeyi, odağı ve imleci ayrı ayrı kontrol edin. Command-Shift-RileReverse rowskomutunu çalıştırın. Düzenlenen değerin özgün öğe kimliğine bağlı kaldığını doğrulayın, ardından odağı ve seçimi kontrol edin.- Gerçek arayüzünüzde satır kaldırmayı, yeniden görünmesini ve programlı metin değiştirmeyi seçtiğiniz geri yükleme politikasına göre sınayın.
Bir düğmeye tıklamak odağı değiştirebilir. Bu nedenle etkileşimden kaynaklanan odak geçişini, model güncellemesinin neden olduğu seçim sıfırlamasından ayırın. Uygulamanızın sunduğu gerçek denetimleri ve klavye komutlarını sınayın.
Kararlı kimlikler ve doğrudan metin bağlamasıyla başlayın. Açık geri yüklemeyi yalnızca gerektiren geçişler için ekleyin ve programlı değiştirmelere tanımlı bir seçim politikası uygulayın. Böylece sıradan etiket düzenleme basit kalırken satır kaldırma ve metin değiştirme sınırları da belirginleşir.
Kaynaklar
Demystify SwiftUI — Apple Developer.
ForEach — Apple Developer Documentation.
id(_:) — Apple Developer Documentation.
FocusState — Apple Developer Documentation.
TextSelection — Apple Developer Documentation.
init(_:text:selection:prompt:axis:) — Apple Developer Documentation.
TextField — Apple Developer Documentation.
macOS Sequoia 15 Release Notes — Apple Developer Documentation.
Strings and Characters — The Swift Programming Language.
NSViewRepresentable — Apple Developer Documentation.
TextKit — Apple Developer Documentation.
selectedRanges — Apple Developer Documentation.
textViewDidChangeSelection(_:) — Apple Developer Documentation.
hasMarkedText() — Apple Developer Documentation.
fieldEditor(_:for:) — Apple Developer Documentation.