Agar highlight pencarian tetap diperbarui di NSTextView yang dibungkus SwiftUI, invalidasikan atribut rendering TextKit 2 saat kueri atau teks berubah, bangun ulang highlight dari konten terkini di validator rendering, lalu minta tampilan digambar ulang.
Panduan ini membuat editor Mac sederhana untuk label sintetis seperti Tile A dan Circle B. Tugas konkretnya adalah menyorot label yang cocok saat seseorang mengubah kolom pencarian atau mengedit dokumen di aplikasi yang sama.
Apa yang perlu diperbarui saat highlight berubah?
Ada dua masukan yang tidak bergantung satu sama lain: karakter dokumen dan kueri highlight. Mengedit dokumen mengubah konten teks. Mengubah kolom pencarian SwiftUI mengubah status aplikasi tanpa harus mengubah apa pun di sistem teks. Pembungkus Anda harus menghubungkan kedua peristiwa ini ke penyorot.
Apple menyediakan renderingAttributesValidator sebagai callback untuk menyediakan atribut rendering bagi fragmen tata letak. Apple juga menyediakan invalidateRenderingAttributes(for:) untuk menginvalidasi atribut tersebut pada rentang teks. Jika digunakan bersama, API ini mendukung rancangan yang menyimpan kueri sebagai status terkini dan membangun ulang highlight saat rendering perlu divalidasi. Validator atribut rendering, Invalidasi atribut rendering.
Menggambar ulang adalah hal yang terpisah. Menetapkan needsDisplay menandai tampilan agar digambar pada giliran loop peristiwa berikutnya. Ini tidak menjelaskan aturan highlight mana yang berubah atau menghitung kecocokan baru. Karena itu, implementasi di bawah menginvalidasi atribut rendering dan meminta tampilan digambar ulang. NSView needsDisplay.
| Peristiwa | Status yang perlu diperbarui | Tindakan |
|---|---|---|
| Kolom pencarian berubah | Kueri terkini milik tampilan teks | Invalidasi atribut rendering dan minta tampilan digambar ulang |
| Teks dokumen berubah | AppKit sudah menyimpan teks yang diedit | Invalidasi highlight melalui delegasi teks |
| Sebuah fragmen perlu divalidasi | Atribut highlight sementara | Hapus latar belakang sebelumnya dan terapkan kecocokan terkini |
| Kueri menjadi kosong | Kueri terkini | Hapus highlight sebelumnya tanpa menambahkan pengganti |
Pertahankan tampilan teks pada TextKit 2
Gunakan target deployment macOS 13 atau lebih baru untuk contoh ini dan inisialisasi tampilan teks secara eksplisit dengan NSTextView(usingTextLayoutManager: true). Apple memperkenalkan konstruktor kemudahan pemilih mesin saat transisi yang dijelaskan dalam sesi WWDC22. API inti AppKit untuk atribut rendering yang digunakan di sini didokumentasikan tersedia mulai macOS 12. Apa yang baru di TextKit dan tampilan teks, NSTextLayoutManager.
Periksa textView.textLayoutManager saat mendiagnosis mesin yang aktif. Mengakses layoutManager, termasuk melalui kontainer teks, dapat memicu mode kompatibilitas TextKit 1. Apple menjelaskan bahwa transisi ini hanya berlaku satu arah untuk tampilan teks yang ada. Jangan gunakan kueri pengelola tata letak lama di jalur TextKit 2. Kompatibilitas dan modernisasi TextKit.
Batas lainnya adalah kepemilikan tata letak SwiftUI. Apple memperingatkan agar tidak langsung mengubah frame atau bounds milik tampilan yang direpresentasikan. Di sini, akar yang direpresentasikan adalah NSScrollView; penetapan frame awal dilakukan pada subview dokumen AppKit-nya. SwiftUI mengendalikan ukuran luar tampilan gulir. NSViewRepresentable.
Buat contoh lengkap yang dapat diedit
Buat app SwiftUI macOS di Xcode, atur target deployment ke macOS 13 atau lebih baru, lalu ganti deklarasi app dan content view yang dibuat otomatis dengan satu file ini. Pastikan hanya ada satu deklarasi app @main di target.
SwiftUI memiliki kueri pencarian. Tampilan teks AppKit memiliki dokumen demonstrasi ini selama masa hidupnya. Pencocokan sengaja dibuat literal, peka huruf besar-kecil, dan tidak tumpang tindih agar contoh ini berperilaku konsisten untuk label sintetis. Ini adalah demonstrasi dalam memori; penyimpanan dokumen tidak diterapkan.
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)
}
}
}
Kode telah diperiksa terhadap deklarasi API yang diperoleh, tetapi belum dikompilasi atau dijalankan dengan AppKit di lingkungan ini. Pemeriksaan deterministik terpisah memverifikasi offset UTF-16 ilustratif yang dibahas di bawah; pemeriksaan tersebut tidak membuktikan perilaku GUI.
Mengapa pembungkus menggunakan dua jalur pembaruan
updateNSView menerima status SwiftUI yang baru dan menyalin kueri yang berubah ke tampilan teks yang sudah ada. Validator membaca kueri tersimpan itu setiap kali berjalan. Referensi lemahnya menunjuk ke tampilan AppKit, bukan menangkap nilai representable awal yang mungkin memuat kueri usang. Apple menetapkan metode pembaruan representable sebagai tempat untuk menerapkan informasi SwiftUI yang baru. Siklus hidup NSViewRepresentable.
Pengetikan di dokumen mengikuti jalur koordinator. Apple mendokumentasikan textDidChange(_:) sebagai notifikasi delegasi bahwa karakter atau atribut pemformatan telah berubah. Koordinator merespons dengan segera memperbarui highlight; binding dokumen tidak diperlukan untuk memicu pembaruan SwiftUI. textDidChange(_:).
Teks contoh awal hanya ditetapkan saat pembuatan. Pembaruan kueri berikutnya tidak mengubah dokumen. Jika nanti Anda menambahkan binding dokumen, bandingkan konten yang masuk sebelum menggantinya dan tentukan sisi mana yang memiliki hak atas pengeditan. Menetapkan ulang teks contoh di setiap pembaruan akan menimpa dokumen pengguna.
Mengapa penghapusan harus dilakukan sebelum pemeriksaan kueri kosong
Setiap validasi menghapus atribut .backgroundColor sebelumnya milik penyorot dari fragmen sebelum mencari kecocokan. Karena itu, kueri kosong tetap menjalankan pembersihan. Ini mengatasi kesalahan implementasi umum ketika pencarian berfungsi sampai kolom dikosongkan, tetapi kumpulan highlight terakhir tetap terlihat.
Contoh ini menggunakan API penghapusan dan penambahan per kunci yang didokumentasikan, sehingga perubahan dibatasi pada atribut latar belakang. Contoh mengasumsikan penyorot ini mengelola latar belakang rendering; jika fitur lain juga menyediakannya, gabungkan aturan tersebut dalam satu kebijakan rendering. Hapus atribut rendering, Tambahkan atribut rendering.
Kecocokan juga diinterseksikan dengan fragmen yang sedang divalidasi. Apple menetapkan rangeInElement relatif terhadap asal dokumen, dan intersection(_:) mengembalikan rentang teks bersama. Dengan begitu, kecocokan hanya menyumbangkan bagian yang termasuk dalam fragmen saat ini. Rentang fragmen, Irisan rentang teks.
Jaga agar offset kecocokan konsisten dengan dokumen terkini
Contoh ini mencari snapshot NSString dan menggunakan NSRange yang dikembalikannya. Apple menetapkan bahwa panjang, indeks, dan rentang NSString menggunakan unit kode UTF-16. Swift juga menyediakan representasi UTF-16 string melalui utf16; jumlah karakter Swift bukan pengganti offset ini. Pengindeksan NSString, String.utf16.
Untuk pemeriksaan ilustratif, tambahkan simbol netral dan spasi 🔷 di awal dokumen contoh. Tile pertama kemudian dimulai pada offset UTF-16 3, dengan panjang 4. Kemunculan keduanya dimulai pada offset 19. Sisipkan X di awal dan lokasi tersebut menjadi 5 dan 21. Ini adalah contoh hasil perhitungan, bukan pengamatan dari AppKit yang dijalankan.
Konversikan setiap kecocokan melalui pengelola konten saat ini: hitung offset dari awal dokumen ke awal kecocokan, lalu dari sana ke akhir kecocokan. Jangan membuat lokasi teks sendiri dari indeks bilangan bulat atau menyimpan lokasi kecocokan lama setelah dokumen diedit. Apple menunjukkan konversi ini melalui pengelola konten, yang API lokasinya dapat mengembalikan nil untuk offset di luar batas. Panduan konversi rentang, Lokasi penyimpanan konten teks.
Loop pencarian bergerak maju menggunakan akhir kecocokan yang benar-benar ditemukan. Apple mendokumentasikan bahwa hasil pencarian relatif terhadap awal string, meskipun pencarian dilakukan dalam rentang yang lebih kecil. Penjagaan kueri kosong dan pergerakan maju dalam contoh ini juga mencegah loop yang tidak bergerak. Untuk fitur Cari yang ditujukan kepada pengguna umum, pilih semantik pencocokan terlokalisasi yang sesuai; Apple merekomendasikan pencarian standar terlokalisasi untuk teks yang ditampilkan kepada pengguna. Pencarian rentang NSString.
Diagnosis highlight usang dengan urutan yang berguna
Gunakan urutan ini untuk membedakan status yang tidak diperbarui dari masalah rendering atau rentang. Ini adalah pemeriksaan yang diharapkan untuk contoh ini, bukan hasil pengujian yang dilaporkan.
- Mulai dengan
Tile. Kedua labelTileseharusnya disorot dengan latar belakang. - Ubah kueri menjadi
Circle. Latar belakang sebelumnya seharusnya menghilang dan labelCirclemenjadi tersorot. - Kosongkan kueri. Semua latar belakang pencarian seharusnya menghilang.
- Pulihkan
Tile, lalu edit salah satu label yang cocok menjadiSquare. Label yang diedit itu seharusnya tidak lagi cocok. - Sisipkan
🔷sebelum label pertama. Kecocokan seharusnya mengikuti teks, bukan tetap berada pada offset bilangan bulat sebelumnya. - Tambahkan baris label sintetis secukupnya agar dokumen dapat digulir. Ubah kueri, lalu gulir dokumen untuk memeriksa fragmen yang baru ditampilkan.
Jika kueri berubah tetapi tidak ada yang diperbarui, periksa updateNSView, kueri yang tersimpan, dan pemanggilan invalidasi terlebih dahulu. Jika pengeditan dokumen gagal sementara perubahan kueri berhasil, periksa penetapan delegasi dan textDidChange(_:). Jika kecocokan sebelumnya tetap berwarna, pastikan pembersihan berjalan sebelum setiap pengembalian awal untuk kueri kosong.
Jika textLayoutManager menjadi nil, selidiki mode kompatibilitas sebelum menambahkan pemanggilan gambar ulang. Apple menyediakan notifikasi peralihan pengelola tata letak untuk mendiagnosis fallback NSTextView. Demonstrasi ini memerlukan TextKit 2 dan tidak menerapkan penyorot TextKit 1. Diagnostik mode kompatibilitas.
Sesuaikan implementasi untuk dokumen yang lebih besar
Implementasi ini mengutamakan jalur pembaruan yang mudah diperiksa: atribut rendering dokumen diinvalidasi dan string saat ini dipindai selama setiap validasi fragmen. Proses ini mengulang pekerjaan dan cocok untuk demonstrasi kecil, bukan beban kerja dokumen besar yang belum diukur.
Untuk dokumen yang lebih besar, hitung kecocokan sekali untuk setiap revisi dokumen dan kueri, lalu buat validasi menginterseksikan kecocokan tersebut dengan tiap fragmen. Kaitkan kecocokan yang di-cache dengan revisi teks yang tepat saat kecocokan itu dihasilkan. Jika pencocokan berjalan secara asinkron, buang hasil yang revisi dokumen atau kuerinya sudah berubah sebelum hasil diterapkan.
Pisahkan pekerjaan rendering dari permintaan geometri untuk seluruh dokumen. Dokumentasi tata letak Apple membedakan invalidasi rendering dari invalidasi tata letak, dan panduan arsitektur TextKit-nya menyarankan agar pekerjaan tata letak difokuskan pada area pandang karena memastikan tata letak di luar layar dapat memakan biaya. Perubahan kueri warna latar belakang sebaiknya dimulai dengan invalidasi rendering, tanpa menambahkan perhitungan tata letak seluruh dokumen secara default. Operasi NSTextLayoutManager, Mengenal TextKit 2.
Pastikan urutan pembaruan tetap jelas saat editor berkembang: terima status terkini, invalidasi atribut rendering yang terdampak, hapus nilai highlight yang sudah usang, lalu bangun ulang kecocokan berdasarkan teks terkini. Periksa perubahan kueri, pengeditan dokumen, pengosongan, dan pengguliran secara terpisah agar setiap kegagalan mengarah ke bagian implementasi tertentu.
Sumber
- NSViewRepresentable — Apple
- NSTextLayoutManager — Apple
- renderingAttributesValidator — Apple
- invalidateRenderingAttributes(for:) — Apple
- needsDisplay — Apple
- Apa yang baru di TextKit dan tampilan teks — 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
- Mengenal TextKit 2 — Apple