Untuk memastikan sorotan carian terus dikemas kini dalam NSTextView yang dibungkus dengan SwiftUI, batalkan kesahan atribut pemaparan TextKit 2 apabila pertanyaan atau teks berubah, bina semula sorotan daripada kandungan semasa dalam validator pemaparan, dan minta paparan semula.

Panduan ini membina editor Mac kecil untuk label sintetik seperti Tile A dan Circle B. Tugas khususnya ialah menyerlahkan label yang sepadan apabila seseorang mengubah medan carian atau mengedit dokumen dalam aplikasi yang sama.

Apakah yang perlu dikemas kini apabila sorotan berubah?

Terdapat dua input berasingan: aksara dokumen dan pertanyaan sorotan. Mengedit dokumen mengubah kandungan teks. Mengubah medan carian SwiftUI mengubah keadaan aplikasi tanpa semestinya mengubah apa-apa dalam sistem teks. Pembungkus anda perlu menghubungkan kedua-dua peristiwa ini kepada penyerlah.

Apple menyediakan renderingAttributesValidator sebagai panggilan balik untuk membekalkan atribut pemaparan bagi fragmen susun atur. Apple juga menyediakan invalidateRenderingAttributes(for:) untuk membatalkan kesahan atribut tersebut pada julat teks. Bersama-sama, API ini menyokong reka bentuk yang menyimpan pertanyaan sebagai keadaan semasa dan membina semula sorotan apabila pemaparan perlu disahkan. Validator atribut pemaparan, Batalkan kesahan atribut pemaparan.

Paparan semula ialah perkara yang berasingan. Menetapkan needsDisplay menandakan bahawa paparan perlu dilukis pada giliran gelung peristiwa seterusnya. Tetapan itu sendiri tidak menerangkan peraturan sorotan yang berubah atau mengira padanan baharu. Oleh itu, pelaksanaan di bawah membatalkan kesahan atribut pemaparan dan meminta paparan semula. NSView needsDisplay.

Peristiwa Keadaan untuk dikemas kini Tindakan
Medan carian berubah Pertanyaan semasa paparan teks Batalkan kesahan atribut pemaparan dan minta paparan semula
Teks dokumen berubah AppKit sudah menyimpan teks yang diedit Batalkan kesahan sorotan melalui perwakilan teks
Fragmen perlu disahkan Atribut sorotan sementara Kosongkan latar belakang sebelumnya dan gunakan padanan semasa
Pertanyaan menjadi kosong Pertanyaan semasa Kosongkan sorotan sebelumnya tanpa menambah pengganti

Kekalkan paparan teks pada TextKit 2

Gunakan sasaran penggunaan macOS 13 atau lebih baharu untuk contoh ini dan mulakan paparan teks secara jelas dengan NSTextView(usingTextLayoutManager: true). Apple memperkenalkan pembina mudah yang memilih enjin ini bersama peralihan yang diterangkan dalam sesi WWDC22. API atribut pemaparan AppKit utama yang digunakan di sini didokumenkan sebagai tersedia mulai macOS 12. Perkara baharu dalam TextKit dan paparan teks, NSTextLayoutManager.

Periksa textView.textLayoutManager apabila mendiagnosis enjin yang aktif. Mengakses layoutManager, termasuk melalui bekas teks, boleh mencetuskan mod keserasian TextKit 1. Apple menerangkan bahawa peralihan ini hanya sehala untuk paparan teks sedia ada. Jangan gunakan pertanyaan pengurus susun atur legasi dalam laluan TextKit 2. Keserasian dan pemodenan TextKit.

Satu lagi batasan ialah pemilikan susun atur oleh SwiftUI. Apple memberi amaran supaya tidak mengubah frame atau bounds paparan yang diwakili secara langsung. Di sini, akar yang diwakili ialah NSScrollView; penetapan bingkai awal dilakukan pada subpaparan dokumen AppKit. SwiftUI mengawal saiz luar paparan tatal. NSViewRepresentable.

Bina contoh boleh sunting yang lengkap

Cipta aplikasi SwiftUI macOS dalam Xcode, tetapkan sasaran penggunaannya kepada macOS 13 atau lebih baharu, dan gantikan pengisytiharan aplikasi dan paparan kandungan yang dijana dengan satu fail ini. Pastikan hanya ada satu pengisytiharan aplikasi @main dalam sasaran.

SwiftUI memiliki pertanyaan carian. Paparan teks AppKit memiliki dokumen demonstrasi ini sepanjang hayatnya. Padanan sengaja dibuat secara literal, peka huruf besar-kecil dan tidak bertindih supaya contoh ini mempunyai tingkah laku yang boleh diramal untuk label sintetik. Ini ialah demonstrasi dalam memori; ia tidak melaksanakan penyimpanan dokumen.

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 ini telah disemak berdasarkan pengisytiharan API yang diperoleh, tetapi belum dikompil atau dijalankan dengan AppKit dalam persekitaran ini. Semakan deterministik berasingan mengesahkan ofset UTF-16 ilustrasi yang dibincangkan di bawah; semakan tersebut tidak membuktikan tingkah laku GUI.

Mengapa pembungkus menggunakan dua laluan kemas kini

updateNSView menerima keadaan SwiftUI yang baharu dan menyalin pertanyaan yang berubah ke dalam paparan teks sedia ada. Validator membaca pertanyaan yang disimpan itu setiap kali ia dijalankan. Rujukan lemahnya menunjuk kepada paparan AppKit, bukannya menangkap nilai representable awal yang mungkin mengandungi pertanyaan lapuk. Apple mentakrifkan kaedah kemas kini representable sebagai tempat untuk menggunakan maklumat SwiftUI yang baharu. Kitar hayat NSViewRepresentable.

Menaip dalam dokumen menggunakan laluan penyelaras. Apple mendokumenkan textDidChange(_:) sebagai pemberitahuan perwakilan bahawa aksara atau atribut pemformatan telah berubah. Penyelaras memberi respons dengan menyegarkan sorotan serta-merta; ia tidak memerlukan ikatan dokumen untuk mencetuskan kemas kini SwiftUI. textDidChange(_:).

Teks sampel awal hanya ditetapkan semasa penciptaan. Kemas kini pertanyaan seterusnya tidak mengusik dokumen. Jika anda kemudian menambah ikatan dokumen, bandingkan kandungan masuk sebelum menggantikannya dan tentukan pihak yang memiliki suntingan. Menetapkan semula teks sampel pada setiap kemas kini akan menimpa dokumen pengguna.

Mengapa pembersihan perlu dilakukan sebelum menyemak pertanyaan kosong

Setiap pengesahan membuang atribut .backgroundColor terdahulu milik penyerlah daripada fragmen sebelum mencari padanan. Oleh itu, pertanyaan kosong tetap menjalankan pembersihan. Ini menangani kesilapan pelaksanaan yang lazim apabila carian berfungsi sehingga medan dikosongkan, lalu set sorotan terakhir terus kelihatan.

Contoh ini menggunakan API penyingkiran dan penambahan mengikut kunci yang didokumenkan, lalu mengehadkan perubahannya kepada atribut latar belakang. Contoh ini mengandaikan penyerlah ini memiliki latar belakang pemaparan; jika ciri lain turut membekalkannya, gabungkan peraturan tersebut dalam satu dasar pemaparan. Buang atribut pemaparan, Tambah atribut pemaparan.

Padanan juga disilangkan dengan fragmen yang sedang disahkan. Apple mentakrifkan rangeInElement relatif kepada asal dokumen, dan intersection(_:) mengembalikan julat teks yang dikongsi. Ini membolehkan padanan menyumbang hanya bahagian yang tergolong dalam fragmen semasa. Julat fragmen, Persilangan julat teks.

Pastikan ofset padanan selaras dengan dokumen semasa

Contoh ini mencari snapshot NSString dan menggunakan NSRange yang dikembalikannya. Apple menyatakan bahawa panjang, indeks dan julat NSString menggunakan unit kod UTF-16. Swift juga mendedahkan perwakilan UTF-16 bagi rentetan melalui utf16; bilangan aksara Swift bukan pengganti bagi ofset ini. Pengindeksan NSString, String.utf16.

Sebagai semakan ilustrasi, tambahkan simbol neutral dan ruang 🔷 pada permulaan dokumen sampel. Tile yang pertama kemudian bermula pada ofset UTF-16 3, dengan panjang 4. Kemunculan keduanya bermula pada ofset 19. Sisipkan X pada permulaan dan lokasi tersebut menjadi 5 dan 21. Ini contoh yang dikira, bukannya pemerhatian daripada pelaksanaan AppKit.

Tukarkan setiap padanan melalui pengurus kandungan semasa: ofset dari permulaan dokumen ke permulaan padanan, kemudian dari situ ke penghujung padanan. Jangan bina lokasi teks daripada indeks integer sendiri atau kekalkan lokasi padanan lama selepas dokumen disunting. Apple menunjukkan penukaran ini melalui pengurus kandungan, yang API lokasinya boleh mengembalikan nil untuk ofset di luar sempadan. Panduan penukaran julat, Lokasi storan kandungan teks.

Gelung carian bergerak ke hadapan menggunakan penghujung padanan sebenar yang dikembalikan. Apple mendokumenkan bahawa hasil carian adalah relatif kepada permulaan rentetan, walaupun carian dilakukan dalam julat yang lebih kecil. Semakan pertanyaan kosong dan pergerakan ke hadapan dalam contoh ini juga menghalang gelung daripada tersekat. Untuk ciri Cari umum yang ditujukan kepada pengguna, pilih semantik padanan setempat yang sesuai; Apple mengesyorkan carian standard setempat untuk teks yang dipaparkan kepada pengguna. Carian julat NSString.

Diagnosis sorotan lapuk mengikut urutan yang berguna

Gunakan urutan ini untuk membezakan kemas kini keadaan yang tiada daripada masalah pemaparan atau julat. Ini ialah semakan yang dijangka bagi contoh ini, bukannya hasil ujian yang dilaporkan.

  1. Mulakan dengan Tile. Kedua-dua label Tile sepatutnya diserlahkan dengan latar belakang.
  2. Tukar pertanyaan kepada Circle. Latar belakang sebelumnya sepatutnya hilang dan label Circle sepatutnya diserlahkan.
  3. Kosongkan pertanyaan. Semua latar belakang carian sepatutnya hilang.
  4. Pulihkan Tile, kemudian edit satu label sepadan menjadi Square. Label yang diedit itu sepatutnya tidak lagi sepadan.
  5. Sisipkan 🔷 sebelum label pertama. Sorotan sepatutnya mengikut teks dan bukannya kekal pada ofset integer sebelumnya.
  6. Tambah baris label sintetik yang mencukupi untuk menatal. Tukar pertanyaan, kemudian tatal dokumen untuk menyemak fragmen yang baru dipaparkan.

Jika pertanyaan berubah tetapi tiada apa-apa dikemas kini, periksa updateNSView, pertanyaan yang disimpan dan panggilan pembatalan kesahan terlebih dahulu. Jika suntingan dokumen gagal sedangkan perubahan pertanyaan berfungsi, periksa penetapan perwakilan dan textDidChange(_:). Jika padanan lama kekal berwarna, pastikan pembersihan dijalankan sebelum setiap pemulangan awal untuk pertanyaan kosong.

Jika textLayoutManager menjadi nil, siasat mod keserasian sebelum menambah panggilan lukis semula. Apple menyediakan pemberitahuan pertukaran pengurus susun atur untuk mendiagnosis peralihan balik NSTextView. Demonstrasi ini memerlukan TextKit 2 dan tidak melaksanakan penyerlah TextKit 1. Diagnosis mod keserasian.

Sesuaikan pelaksanaan untuk dokumen yang lebih besar

Pelaksanaan ini mengutamakan laluan kemas kini yang mudah diperiksa: ia membatalkan kesahan atribut pemaparan dokumen dan mengimbas rentetan semasa semasa setiap pengesahan fragmen. Ini mengulangi kerja dan sesuai untuk demonstrasi kecil, bukannya beban kerja dokumen besar yang belum diukur.

Untuk dokumen yang lebih besar, kira padanan sekali bagi setiap semakan dokumen dan pertanyaan, kemudian minta pengesahan menyilangkan padanan tersebut dengan setiap fragmen. Kaitkan padanan yang dicache dengan semakan teks tepat yang menghasilkannya. Jika pemadanan berjalan secara tak segerak, buang hasil yang semakan dokumen atau pertanyaannya telah berubah sebelum hasil itu digunakan.

Asingkan kerja pemaparan daripada permintaan geometri seluruh dokumen. Dokumentasi susun atur Apple membezakan pembatalan kesahan pemaparan daripada pembatalan kesahan susun atur, dan panduan seni bina TextKit mengesyorkan agar kerja susun atur ditumpukan pada ruang paparan kerana memastikan susun atur untuk kandungan di luar skrin boleh menjadi mahal. Perubahan pertanyaan warna latar belakang sepatutnya bermula dengan pembatalan kesahan pemaparan, tanpa menambah pengiraan susun atur seluruh dokumen secara lalai. Operasi NSTextLayoutManager, Kenali TextKit 2.

Pastikan urutan kemas kini jelas apabila editor berkembang: terima keadaan semasa, batalkan kesahan atribut pemaparan yang terjejas, kosongkan nilai sorotan yang sudah lapuk dan bina semula padanan berdasarkan teks semasa. Sahkan perubahan pertanyaan, suntingan dokumen, tindakan mengosongkan dan penatalan secara berasingan supaya setiap kegagalan menunjukkan bahagian pelaksanaan yang khusus.

Sumber