SwiftUI ile sarmalanmış bir NSTextView’de arama vurgularını güncel tutmak için sorgu veya metin değiştiğinde TextKit 2 oluşturma özniteliklerini geçersiz kılın, vurguları güncel içerikten oluşturma doğrulayıcısında yeniden oluşturun ve yeniden çizim isteyin.
Bu anlatımda Tile A ve Circle B gibi yapay etiketler için küçük bir Mac düzenleyicisi oluşturuluyor. Amaç, kullanıcı arama alanını değiştirirken veya aynı uygulamada belgeyi düzenlerken eşleşen etiketleri vurgulamaktır.
Vurgu değiştiğinde nelerin güncellenmesi gerekir?
Birbirinden bağımsız iki girdi vardır: belgenin karakterleri ve vurgu sorgusu. Belgeyi düzenlemek metin içeriğini değiştirir. SwiftUI arama alanını değiştirmek ise metin sisteminde herhangi bir değişikliğe yol açmadan uygulama durumunu değiştirebilir. Sarmalayıcınız her iki olayı da vurgulayıcıya iletmelidir.
Apple, bir düzen parçası için oluşturma özniteliklerini sağlama geri çağrısı olarak renderingAttributesValidator API’sini sunar. Ayrıca bu öznitelikleri bir metin aralığında geçersiz kılmak için invalidateRenderingAttributes(for:) API’sini sağlar. Bu API’ler birlikte, sorgunun güncel durum olarak saklandığı ve oluşturma doğrulanması gerektiğinde vurguların yeniden oluşturulduğu bir tasarımı destekler. Oluşturma öznitelikleri doğrulayıcısı, Oluşturma özniteliklerini geçersiz kılma.
Yeniden çizim ayrı bir konudur. needsDisplay değerini ayarlamak, görünümü sonraki olay döngüsü geçişinde çizilmek üzere işaretler. Bu işlem, hangi vurgu kuralının değiştiğini belirtmez veya yeni eşleşmeleri hesaplamaz. Bu nedenle aşağıdaki uygulama, oluşturma özniteliklerini geçersiz kılar ve yeniden çizim ister. NSView needsDisplay.
| Olay | Güncellenecek durum | Eylem |
|---|---|---|
| Arama alanı değişir | Metin görünümünün güncel sorgusu | Oluşturma özniteliklerini geçersiz kılın ve yeniden çizim isteyin |
| Belge metni değişir | AppKit, düzenlenmiş metni zaten tutar | Metin temsilcisi aracılığıyla vurguları geçersiz kılın |
| Bir parçanın doğrulanması gerekir | Parçanın geçici vurgu öznitelikleri | Önceki arka planı temizleyin ve güncel eşleşmeleri uygulayın |
| Sorgu boşalır | Güncel sorgu | Yeni vurgular eklemeden önceki vurguları temizleyin |
Metin görünümünü TextKit 2’de tutun
Bu örnek için macOS 13 veya sonraki bir sürümü hedefleyin ve metin görünümünü açıkça NSTextView(usingTextLayoutManager: true) ile başlatın. Apple, WWDC22 oturumunda anlattığı geçişle birlikte motoru seçen kullanışlı başlatıcıları kullanıma sundu. Burada kullanılan temel AppKit oluşturma özniteliği API’lerinin macOS 12’den itibaren kullanılabildiği belgelenmiştir. TextKit ve metin görünümlerindeki yenilikler, NSTextLayoutManager.
Etkin motoru incelerken textView.textLayoutManager değerini kontrol edin. Metin kapsayıcısı üzerinden yapılanlar da dahil olmak üzere layoutManager erişimi, TextKit 1 uyumluluk kipini tetikleyebilir. Apple, mevcut metin görünümü için bu geçişin tek yönlü olduğunu belirtir. TextKit 2 yolunda eski düzen yöneticisi sorgularını kullanmayın. TextKit uyumluluğu ve modernizasyonu.
Diğer sınır, SwiftUI’nin düzen üzerindeki denetimidir. Apple, temsil edilen görünümün frame veya bounds değerlerini doğrudan değiştirmemeyi önerir. Burada temsil edilen kök bir NSScrollView’dur; başlangıç çerçevesi ataması, onun AppKit belge alt görünümüne aittir. Kaydırma görünümünün dış boyutunu SwiftUI belirler. NSViewRepresentable.
Düzenlenebilir eksiksiz bir örnek oluşturun
Xcode’da bir macOS SwiftUI uygulaması oluşturun, hedef sürümünü macOS 13 veya üstü olarak ayarlayın ve oluşturulan uygulama ile içerik görünümü bildirimlerini bu tek dosyayla değiştirin. Hedefte yalnızca bir @main uygulama bildirimi bulunsun.
Arama sorgusunun sahibi SwiftUI’dir. AppKit metin görünümü ise bu gösterim belgesinin ömrü boyunca ona sahip olur. Öngörülebilir yapay etiket davranışı için eşleştirme bilerek düz metin, büyük/küçük harfe duyarlı ve çakışmayan biçimde yapılır. Bu, bellekte çalışan bir gösterimdir; belge kaydetmeyi uygulamaz.
import SwiftUI
import AppKit
@main
struct HighlightDemoApp: App {
var body: some Scene {
WindowGroup { ContentView() }
}
}
struct ContentView: View {
@State private var query = "Tile"
var body: some View {
VStack {
TextField("Highlight label", text: $query)
HighlightEditor(query: query)
}
.padding()
.frame(minWidth: 480, minHeight: 300)
}
}
struct HighlightEditor: NSViewRepresentable {
let query: String
func makeCoordinator() -> Coordinator { Coordinator() }
func makeNSView(context: Context) -> NSScrollView {
let scroll = NSScrollView()
scroll.hasVerticalScroller = true
let view = HighlightTextView(usingTextLayoutManager: true)
view.frame = scroll.contentView.bounds
view.autoresizingMask = [.width]
view.isVerticallyResizable = true
view.isHorizontallyResizable = false
view.textContainer?.widthTracksTextView = true
view.textContainer?.containerSize = NSSize(
width: scroll.contentSize.width,
height: CGFloat.greatestFiniteMagnitude
)
view.isRichText = false
view.font = .systemFont(ofSize: 16)
view.string = "Tile A\nCircle B\nTile C\nSquare D"
view.delegate = context.coordinator
scroll.documentView = view
guard let manager = view.textLayoutManager else {
preconditionFailure("This demo requires TextKit 2")
}
manager.renderingAttributesValidator = { [weak view] manager, fragment in
view?.render(manager, fragment: fragment)
}
view.query = query
view.refreshHighlights()
return scroll
}
func updateNSView(_ scroll: NSScrollView, context: Context) {
guard let view = scroll.documentView as? HighlightTextView,
view.query != query else { return }
view.query = query
view.refreshHighlights()
}
@MainActor
final class Coordinator: NSObject, NSTextViewDelegate {
func textDidChange(_ notification: Notification) {
(notification.object as? HighlightTextView)?.refreshHighlights()
}
}
}
@MainActor
final class HighlightTextView: NSTextView {
var query = ""
func refreshHighlights() {
guard let manager = textLayoutManager else { return }
manager.invalidateRenderingAttributes(for: manager.documentRange)
needsDisplay = true
}
func render(_ manager: NSTextLayoutManager, fragment: NSTextLayoutFragment) {
let fragmentRange = fragment.rangeInElement
manager.removeRenderingAttribute(.backgroundColor, for: fragmentRange)
guard !query.isEmpty, let content = manager.textContentManager else { return }
let text = string as NSString
var cursor = 0
while cursor < text.length {
let match = text.range(of: query, options: [.literal],
range: NSRange(location: cursor, length: text.length - cursor))
guard match.location != NSNotFound else { break }
guard let start = content.location(content.documentRange.location,
offsetBy: match.location),
let end = content.location(start, offsetBy: match.length),
let range = NSTextRange(location: start, end: end) else {
assertionFailure("Match does not map to current text content")
return
}
if let part = range.intersection(fragmentRange), !part.isEmpty {
manager.addRenderingAttribute(.backgroundColor,
value: NSColor.systemYellow.withAlphaComponent(0.35), for: part)
}
cursor = NSMaxRange(match)
}
}
}
Kod, alınan API bildirimlerine göre incelendi; ancak bu ortamda AppKit ile derlenip çalıştırılmadı. Aşağıda anlatılan örnek UTF-16 uzaklıklarını ayrı deterministik kontroller doğruladı; bu kontroller GUI davranışını kanıtlamaz.
Sarmalayıcı neden iki güncelleme yolu kullanıyor?
updateNSView, yeni SwiftUI durumunu alır ve değişen sorguyu mevcut metin görünümüne kopyalar. Doğrulayıcı, her çalıştığında saklanan sorguyu okur. Zayıf başvurusu, ilk representable değerini yakalamak yerine AppKit görünümünü gösterir; çünkü ilk değer güncel olmayan bir sorgu içerebilir. Apple, representable’ın güncelleme yöntemini yeni SwiftUI bilgilerini uygulama yeri olarak tanımlar. NSViewRepresentable yaşam döngüsü.
Belgede yazı yazmak koordinatör yolunu izler. Apple, karakterlerin veya biçim özniteliklerinin değiştiğini bildiren bir temsilci bildirimi olarak textDidChange(_:) yöntemini belgeler. Koordinatör, vurguları hemen yenileyerek yanıt verir; SwiftUI güncellemesini tetiklemek için belge bağlamasına ihtiyaç duymaz. textDidChange(_:).
Örnek metin yalnızca oluşturma sırasında atanır. Sonraki sorgu güncellemeleri belgeye dokunmaz. Daha sonra bir belge bağlaması eklerseniz, gelen içeriği değiştirmeden önce karşılaştırın ve düzenlemelerin hangi tarafta yönetileceğini belirleyin. Her güncellemede örnek metni yeniden atamak kullanıcının belgesinin üzerine yazar.
Boş sorgu kontrolünden önce temizleme neden yapılmalı?
Her doğrulama, eşleşmeleri aramadan önce parçadaki vurgulayıcının önceki .backgroundColor özniteliğini kaldırır. Bu nedenle sorgu boş olsa bile temizleme yapılır. Böylece aramanın çalışıp alan temizlendiğinde son vurguların görünür kalmasına yol açan yaygın uygulama hatası önlenir.
Örnek, yalnızca arka plan özniteliğine yönelik değişiklikleri sınırlamak için belgelenen anahtar başına kaldırma ve ekleme API’lerini kullanır. Bu vurgulayıcının oluşturma arka planlarının sahibi olduğunu varsayar; başka bir özellik de arka plan sağlıyorsa bu kuralları tek bir oluşturma politikasında birleştirin. Oluşturma özniteliğini kaldırma, Oluşturma özniteliği ekleme.
Eşleşmeler ayrıca doğrulanan parçayla kesiştirilir. Apple, rangeInElement değerini belgenin başlangıcına göre tanımlar; intersection(_:) ise paylaşılan metin aralığını döndürür. Böylece bir eşleşmenin yalnızca geçerli parçaya ait bölümü hesaba katılır. Parça aralığı, Metin aralığı kesişimi.
Eşleşme uzaklıklarını güncel belgeyle tutarlı kullanın
Örnek, NSString anlık görüntüsünde arama yapar ve döndürülen NSRange değerini kullanır. Apple, NSString uzunluklarının, dizinlerinin ve aralıklarının UTF-16 kod birimleri kullandığını belirtir. Swift de bir dizgenin UTF-16 gösterimini utf16 üzerinden sunar; Swift karakter sayısı bu uzaklıkların yerine kullanılamaz. NSString dizinleme, String.utf16.
Açıklayıcı bir kontrol için örnek belgenin başına nötr 🔷 simgesini ve boşluğu ekleyin. İlk Tile, 3 UTF-16 uzaklığında başlar ve uzunluğu 4 olur. İkinci örnek 19 uzaklığında başlar. Başlangıca X ekleyin; bu konumlar 5 ve 21 olur. Bunlar hesaplanmış örneklerdir; AppKit çalıştırmasında gözlemlenmiş sonuçlar değildir.
Her eşleşmeyi güncel içerik yöneticisi üzerinden dönüştürün: belge başlangıcından eşleşmenin başlangıcına, ardından oradan eşleşmenin bitişine kadar olan uzaklık. Tamsayı dizinlerinden kendiniz metin konumları oluşturmayın veya belge düzenlendikten sonra eski eşleşme konumlarını saklamayın. Apple bu dönüşümü içerik yöneticisi üzerinden gösterir; bu yöneticinin konum API’si, sınırları aşan bir uzaklık için nil döndürebilir. Aralık dönüştürme yönergeleri, Metin içeriği depolama konumları.
Arama döngüsü, döndürülen gerçek eşleşme bitişini kullanarak ilerler. Apple, daha küçük bir aralıkta arama yapılsa bile arama sonuçlarının dizgenin başlangıcına göre olduğunu belgeler. Örneğin boş sorgu kontrolü ve ileriye doğru ilerleme, döngünün ilerlemeden tekrarlanmasını da önler. Kullanıcıya sunulan genel bir Bul özelliği için uygun yerelleştirilmiş eşleştirme kurallarını seçin; Apple, kullanıcılara sunulan metinler için yerelleştirilmiş standart arama yöntemini önerir. NSString aralık araması.
Eski vurguları yararlı bir sırayla inceleyin
Eksik durum güncellemesini oluşturma veya aralık sorunundan ayırmak için şu sırayı izleyin. Bunlar örnek için beklenen kontrollerdir; bildirilmiş test sonuçları değildir.
Tileile başlayın. Her ikiTileetiketi de arka plan vurgusuna sahip olmalıdır.- Sorguyu
Circleolarak değiştirin. Önceki arka planlar kaybolmalı veCircleetiketi vurgulanmalıdır. - Sorguyu temizleyin. Tüm arama arka planları kaybolmalıdır.
Tilesorgusunu geri yükleyin, ardından eşleşen etiketlerden biriniSquareolarak düzenleyin. Düzenlenen etiket artık eşleşmemelidir.- İlk etiketin önüne
🔷ekleyin. Eşleşmeler, eski tamsayı uzaklıklarında kalmak yerine metni izlemelidir. - Kaydırma gerektirecek kadar çok yapay etiket satırı ekleyin. Sorguyu değiştirin, ardından yeni görüntülenen parçaları kontrol etmek için belgede kaydırma yapın.
Sorgu değişiyor ama hiçbir şey güncellenmiyorsa önce updateNSView değerini, saklanan sorguyu ve geçersiz kılma çağrısını inceleyin. Sorgu değişiklikleri çalışırken belge düzenlemeleri çalışmıyorsa temsilci atamasını ve textDidChange(_:) değerini inceleyin. Önceki eşleşmeler renkli kalıyorsa boş sorgu için erken dönen her noktadan önce temizleme işleminin yapıldığını kontrol edin.
textLayoutManager değeri nil olursa yeniden çizim çağrıları eklemeden önce uyumluluk kipini araştırın. Apple, NSTextView’ın geri dönüşünü tanılamak için düzen yöneticisi değiştirme bildirimleri sunar. Bu gösterim TextKit 2’yi gerektirir ve TextKit 1 vurgulayıcısı uygulamaz. Uyumluluk kipi tanılama.
Uygulamayı daha büyük belgeler için uyarlayın
Bu uygulama, kolayca incelenebilir bir güncelleme yoluna öncelik verir: belgenin oluşturma özniteliklerini geçersiz kılar ve her parça doğrulamasında güncel dizgeyi tarar. Bu, aynı işi tekrarlar ve ölçülmemiş büyük belge iş yükleri için değil, küçük bir gösterim için uygundur.
Daha büyük belgelerde eşleşmeleri belge ve sorgu sürümü başına bir kez hesaplayın, ardından doğrulama sırasında bu eşleşmeleri her parçayla kesiştirin. Önbelleğe alınan eşleşmeleri, onları oluşturan tam metin sürümüyle ilişkilendirin. Eşleştirme eşzamansız çalışıyorsa uygulamadan önce belge veya sorgu sürümü değişmiş olan sonuçları atın.
Oluşturma işlerini belge genelindeki geometri isteklerinden ayrı tutun. Apple’ın düzen belgeleri, oluşturma geçersiz kılmayı düzen geçersiz kılmadan ayırır; TextKit mimarisi yönergeleri de ekran dışındaki düzeni tamamlamanın pahalı olabileceğinden düzen işini görünüm alanına odaklamayı önerir. Arka plan rengini değiştiren bir sorgu, varsayılan olarak tam belge düzeni hesaplamaları eklemeden, oluşturma geçersiz kılmayla başlamalıdır. NSTextLayoutManager işlemleri, TextKit 2 ile tanışın.
Düzenleyici büyüdükçe güncelleme sırasını açıkça koruyun: güncel durumu alın, etkilenen oluşturma özniteliklerini geçersiz kılın, artık geçerli olmayan vurgu değerlerini temizleyin ve eşleşmeleri güncel metne göre yeniden oluşturun. Her sorunun uygulamanın belirli bir bölümüne işaret etmesi için sorgu değişikliklerini, belge düzenlemelerini, temizlemeyi ve kaydırmayı ayrı ayrı doğrulayın.
Kaynaklar
- NSViewRepresentable — Apple
- NSTextLayoutManager — Apple
- renderingAttributesValidator — Apple
- invalidateRenderingAttributes(for:) — Apple
- needsDisplay — Apple
- TextKit ve metin görünümlerindeki yenilikler — Apple
- textDidChange(_:) — Apple
- removeRenderingAttribute(_:for:) — Apple
- addRenderingAttribute(_:value:for:) — Apple
- rangeInElement — Apple
- intersection(_:) — Apple
- NSString — Apple
- utf16 — Apple
- location(_:offsetBy:) — Apple
- range(of:options:range:) — Apple
- TextKit 2 ile tanışın — Apple