Untuk mendedahkan titik sisipan dalam NSTextView terbungkus aplikasi Mac anda, pulihkan fokus papan kekunci dan panggil scrollRangeToVisible(_:) dengan pilihan semasanya. Konfigurasikan warna karet secara berasingan. Memastikan karet kekal pada skrin dan menghentikannya daripada berkelip memerlukan kawalan yang berbeza.

Panduan ini membina editor setempat yang ringkas dengan butang Reveal insertion point. Ia menangani tugas desktop yang lazim: kembali ke kedudukan penyuntingan selepas menatal ke bahagian lain dalam dokumen. Pelaksanaan ini mengekalkan pilihan semasa dan membolehkan AppKit mengurus input teks biasa serta pemaparan kursor.

Kenal pasti sebab titik sisipan tiada

Mulakan dengan gejala yang kelihatan. Tatalan, fokus, pilihan dan animasi kursor boleh menghasilkan kesan yang kelihatan serupa, tetapi setiap satunya memerlukan tindak balas yang berbeza.

Perkara yang anda perhatikan Perkara yang perlu diperiksa Tindakan yang sesuai
Kedudukan penyuntingan anda berada di luar kawasan dokumen yang kelihatan Pilihan semasa paparan teks Tatal julat itu sehingga kelihatan
Mengklik kawalan lain menghentikan penaipan dalam editor Responder pertama tetingkap Kembalikan fokus secara jelas apabila diminta
Karet sukar dibezakan daripada latar belakang Warna karet dan latar belakang Konfigurasikan sifat warna paparan teks
Teks diserlahkan dan bukannya menunjukkan satu karet Sama ada julat yang dipilih mempunyai panjang bukan sifar Kekalkan pilihan melainkan pengguna meminta kedudukan dipindahkan
Karet hilang dan muncul semula semasa editor aktif Animasi kursor biasa Kendalikan kelipan secara berasingan daripada tatalan

Apple mendokumenkan scrollRangeToVisible(_:) sebagai menatal paparan tatalan yang mengandungi sehingga aksara pertama julat yang diberikan kelihatan. Ia tidak menjanjikan untuk memusatkan julat itu atau memaparkan keseluruhan pilihan yang panjang. Oleh itu, kaedah ini sesuai untuk mendedahkan kedudukan sisipan tanpa perlu mengira segi empat tepat glif sendiri. Dokumentasi tatalan Apple.

Fokus dikendalikan oleh tetingkap yang menampungnya. Apple secara jelas mengarahkan pemanggil supaya menggunakan window.makeFirstResponder(textView) dan bukannya memanggil becomeFirstResponder() paparan teks secara langsung. Pelaksanaan di bawah mengikuti laluan itu dan menyemak objek yang benar-benar menjadi responder pertama. Dokumentasi NSTextView Apple, dokumentasi peralihan responder pertama.

Bina editor setempat dengan arahan dedah

Cipta projek App macOS dalam Xcode menggunakan SwiftUI dan Swift. Tetapkan sasaran deployment kepada macOS 13 atau lebih baharu untuk contoh ini. Gantikan titik masuk app yang dijana dengan fail pertama di bawah, dan tambahkan fail kedua pada sasaran app yang sama. Kedua-dua fail ini membentuk app demonstrasi yang lengkap.

Contoh ini menggunakan teks sintetik dan mengekalkan suntingan dalam paparan AppKit sepanjang hayat paparan itu. Ia tidak mempunyai pelaksanaan penyimpanan dokumen atau model teks luaran. Menutup app akan membuang suntingan. Kod ini disemak berdasarkan dokumentasi yang diperoleh; ia tidak dikompil atau dijalankan di sini kerana persekitaran yang tersedia ialah Linux dan tiada SDK AppKit.

Cipta tetingkap SwiftUI

Butang menambah nombor permintaan. Ini memberikan pembalut arahan khusus untuk diproses, dan mengelakkan setiap kemas kini SwiftUI memfokuskan semula atau menatal editor.

// CaretDemoApp.swift
import SwiftUI

@main
struct CaretDemoApp: App {
    var body: some Scene {
        WindowGroup("Local Editor") {
            EditorScreen()
        }
    }
}

struct EditorScreen: View {
    @State private var revealRequest = 0

    private let sampleText = (1...80)
        .map { "Sample row \($0): blue square, green circle." }
        .joined(separator: "\n")

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Button("Reveal insertion point") {
                revealRequest += 1
            }

            CaretEditor(
                initialText: sampleText,
                revealRequest: revealRequest
            )
        }
        .padding()
        .frame(minWidth: 480, minHeight: 320)
    }
}

Balut editor AppKit yang boleh ditatal

NSViewRepresentable ialah jambatan Apple untuk mengurus paparan AppKit dalam hierarki SwiftUI. Di sini, paparan yang diwakilkan ialah NSScrollView, dengan NSTextView sebagai paparan dokumennya. Koordinator mengingati permintaan terakhir yang dikendalikan. Apple menyediakan kilang paparan teks boleh tatal yang digunakan semasa penciptaan. NSViewRepresentable, scrollableTextView().

// CaretEditor.swift
import AppKit
import SwiftUI

@MainActor
struct CaretEditor: NSViewRepresentable {
    let initialText: String
    let revealRequest: Int

    @MainActor
    final class Coordinator {
        var lastHandledRequest: Int

        init(lastHandledRequest: Int) {
            self.lastHandledRequest = lastHandledRequest
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(lastHandledRequest: revealRequest)
    }

    func makeNSView(context: Context) -> NSScrollView {
        let scrollView = NSTextView.scrollableTextView()
        scrollView.hasVerticalScroller = true

        guard let textView = scrollView.documentView as? NSTextView else {
            preconditionFailure("Expected an NSTextView document view")
        }

        textView.isRichText = false
        textView.isEditable = true
        textView.isSelectable = true
        textView.allowsUndo = true
        textView.drawsBackground = true
        textView.backgroundColor = .textBackgroundColor
        textView.textColor = .textColor
        textView.insertionPointColor = .textColor
        textView.font = .systemFont(ofSize: 15)
        textView.textContainerInset = NSSize(width: 12, height: 12)
        textView.string = initialText

        let end = (initialText as NSString).length
        textView.setSelectedRange(NSRange(location: end, length: 0))
        return scrollView
    }

    func updateNSView(_ scrollView: NSScrollView, context: Context) {
        guard revealRequest != context.coordinator.lastHandledRequest else {
            return
        }
        context.coordinator.lastHandledRequest = revealRequest

        guard let textView = scrollView.documentView as? NSTextView,
              let window = textView.window,
              textView.acceptsFirstResponder else {
            return
        }

        guard window.makeFirstResponder(textView),
              window.firstResponder === textView else {
            return
        }

        textView.scrollRangeToVisible(textView.selectedRange())
    }
}

Editor bermula dengan pilihan sepanjang sifar di hujung teks sampelnya. Menekan Reveal insertion point memfokuskan editor dan mendedahkan kedudukan itu. Tekanan seterusnya menggunakan pilihan langsung, jadi mengalihkan karet ke tempat lain akan mengubah kedudukan yang didedahkan oleh arahan itu. Arahan ini tidak menggantikan teks atau mengalihkan pilihan.

Kedudukan hujung menggunakan NSString.length, yang mengira unit kod UTF-16. Paparan UTF-16 Swift sepadan dengan API NSString berindeks; String.count Swift pula mengira aksara. Semasa membina julat teks AppKit, pastikan perwakilan itu konsisten. Membaca selectedRange() sedia ada mengelakkan pengiraan ofset untuk arahan dedah itu sendiri. NSString length, Swift String.UTF16View, Swift String.count.

Kekalkan keadaan penyuntingan semasa kemas kini SwiftUI

Pilihan reka bentuk utama ialah memisahkan penciptaan daripada arahan. makeNSView mengisi editor sekali sahaja. updateNSView bertindak balas apabila nombor permintaan berubah dan membiarkan kandungan dokumen tanpa perubahan. Ini mengikuti kitar hayat representable yang Apple huraikan untuk mencipta dan mengemas kini paparan AppKit. SwiftUI juga mengawal reka letak paparan yang diwakilkan, jadi pembalut ini tidak menetapkan bingkai atau sempadan paparan tatalan. Panduan kitar hayat representable dan reka letak Apple.

Elakkan daripada menetapkan textView.string = initialText pada setiap kemas kini dalam demonstrasi ini. Tindakan itu akan menggantikan dokumen semasa pengguna dengan sampel asal. Begitu juga, menetapkan pilihan secara tidak bersyarat ke hujung dokumen akan menyebabkan operasi dedah mengalihkan kedudukan penyuntingan.

Untuk editor yang disokong oleh keadaan SwiftUI, tambahkan penyegerakan dua hala yang disengajakan: hantarkan suntingan pengguna melalui koordinator dan tentukan bila perubahan model yang masuk akan menggantikan teks. Kekalkan pilihan yang sah merentas penggantian itu dan tetapkan cara perubahan serentak diselesaikan. Tanggungjawab ini berasingan daripada mendedahkan karet; sampel ini sengaja mengekalkan satu pemilik bagi kandungan yang boleh disunting. Apple mendokumenkan koordinator sebagai mekanisme untuk menghantar interaksi delegat dan sasaran-tindakan ke dalam SwiftUI. Panduan koordinator.

Pemeriksaan fokus juga penting. makeFirstResponder(_:) boleh gagal apabila responder sedia ada enggan melepaskan fokus. Apple juga menyatakan bahawa kejayaan pemanggilan boleh menyebabkan tetingkap itu sendiri menjadi responder pertama jika responder yang diminta tidak menerima fokus. Memeriksa window.firstResponder === textView mengesahkan hasil sebenar sebelum menatal. Nilai pulangan dan tingkah laku pemulangan responder pertama.

Jika editor belum dilampirkan pada tetingkap, sampel ini menggunakan permintaan itu tanpa bertindak. Tekan butang sekali lagi selepas ia dilampirkan. Untuk ciri fokus awal, reka bentuk permintaan yang mengambil kira pelampiran secara jelas, dan bukannya berulang kali mengambil semula fokus semasa kemas kini yang tidak berkaitan.

Konfigurasikan warna tanpa menggantikan pemaparan kursor

Contoh ini menetapkan .textColor untuk teks dan titik sisipan, serta .textBackgroundColor untuk latar belakang editor. Ini ialah warna semantik yang didokumenkan untuk teks dan kawasan di belakangnya. insertionPointColor ialah sifat yang didokumenkan untuk menetapkan warna karet. Periksa rupa yang terhasil pada setiap versi macOS dan penampilan yang disokong oleh app anda. Warna karet, warna teks, warna latar belakang teks.

Konfigurasi warna tidak meminta fokus, menatal dokumen atau memilih mod paparan tanpa kelipan. Asingkan tanggungjawab tersebut semasa mendiagnosis karet yang hilang. Contohnya, mengubah warna tidak dapat mendedahkan kedudukan yang masih berada di luar kawasan paparan tatalan yang kelihatan.

Sampel ini juga mengelakkan akses kepada layoutManager untuk mengira geometri karet. Apple mendokumenkan bahawa mengakses sifat ini pada paparan teks atau bekas teks boleh menukar paparan kepada mod keserasian yang menggunakan NSLayoutManager pada macOS 12 dan lebih baharu. Memanggil API tatalan aras tinggi mengelakkan perubahan keserasian TextKit ini semata-mata untuk mendedahkan pilihan. Tingkah laku keserasian TextKit.

Fahami batasan karet yang sentiasa kelihatan

Jika maksud “kekalkan kelihatan” ialah “jangan sekali-kali berkelip,” arahan tatalan tidak memenuhi keperluan itu. Dokumentasi kursor moden Apple membezakan animasi automatik daripada mod paparan yang sentiasa kelihatan.

Pada macOS 14 dan lebih baharu, NSTextInsertionIndicator menyediakan mod paparan .automatic, .visible dan .hidden. Apple mendokumenkan .visible untuk memaparkan kursor secara berterusan dengan animasi dinyahdayakan, seperti ketika menunjukkan destinasi seretan. Paparan teks AppKit standard menggunakan UI kursor sistem secara automatik. Panduan penggunaan kursor teks sistem, NSTextInsertionIndicator.

Laluan kursor tersuai yang didokumenkan memerlukan penciptaan dan penentududukan penunjuk anda sendiri. Mod paparannya mengawal tika itu; panduan Apple tidak menyediakan tetapan pada NSTextView untuk menukar penunjuk yang diurus secara dalaman kepada .visible. Anggap laluan tersuai sebagai pilihan seni bina untuk UI teks tersuai, yang turut memerlukan tanggungjawab mengemas kini geometri kursor dan keadaan aktif. Ia bukan perubahan yang boleh terus digunakan pada pembalut ini.

Dua API yang kelihatan lama tidak menyediakan tetapan yang tiada itu. shouldDrawInsertionPoint didokumenkan sebagai baca sahaja. drawInsertionPoint(in:color:turnedOn:) melukis atau memadam kursor, dan Apple menyatakan bahawa pemanggil tidak sepatutnya perlu memanggilnya secara langsung. Kedua-duanya tidak didokumenkan sebagai suis untuk paparan sentiasa kelihatan. Keadaan pelukisan titik sisipan, Kaedah pelukisan titik sisipan.

Untuk tugas editor biasa dalam artikel ini, kekalkan animasi kursor AppKit dan sediakan cara yang boleh diharap untuk kembali ke kedudukan semasa. Pelaksanaan kursor tersuai memerlukan reka bentuk tersendiri dan, jika menggunakan NSTextInsertionIndicator, batas ketersediaan macOS 14.

Sahkan tingkah laku dalam app anda

Gunakan sampel ini untuk memisahkan pemeriksaan tatalan, pilihan, fokus dan rupa. Perkara berikut ialah tingkah laku yang dijangka berdasarkan pelaksanaan, bukannya hasil pelaksanaan yang dilaporkan.

  1. Lancarkan app dan tekan Reveal insertion point. Editor sepatutnya menerima fokus dan mendedahkan kedudukan hujungnya.
  2. Klik dalam baris terdahulu untuk mengalihkan karet. Tatal jauh tanpa mengklik kedudukan teks lain, kemudian tekan butang. Kedudukan terdahulu itu sepatutnya didedahkan.
  3. Pilih beberapa perkataan dan tekan butang. Pilihan sepatutnya kekal dipilih; arahan ini tidak meruntuhkannya menjadi karet.
  4. Taip label tersendiri seperti segi tiga jingga, kemudian tekan butang beberapa kali. Kandungan yang disunting sepatutnya kekal utuh.
  5. Ubah saiz tetingkap dan ulangi operasi dedah. Arahan ini sepatutnya tetap menggunakan julat semasa dan bukannya koordinat skrin lama.
  6. Periksa penampilan cerah dan gelap pada keluaran macOS yang disokong. Pastikan warna teks, latar belakang dan karet kekal jelas apabila digunakan bersama.

Jika tatalan berjaya tetapi karet masih kelihatan berselang-seli, periksa sama ada anda sedang melihat kelipan biasa atau pilihan teks yang aktif. Jika arahan itu tidak berbuat apa-apa, periksa dahulu pengawal pelampiran tetingkap dan responder pertama. Pastikan pembaikan berkait dengan langkah yang gagal: kekalkan teks, pulihkan fokus apabila diminta dan dedahkan julat semasa.

Sumber