Untuk menampilkan titik penyisipan di NSTextView yang dibungkus dalam app Mac Anda, pulihkan fokus keyboard dan panggil scrollRangeToVisible(_:) dengan seleksi saat ini. Atur warna caret secara terpisah. Menjaga caret tetap terlihat di layar dan menghentikan kedipannya memerlukan kontrol yang berbeda.
Panduan ini membuat editor lokal sederhana dengan tombol Reveal insertion point. Panduan ini membahas tugas desktop yang lazim: kembali ke posisi pengeditan setelah menggulir ke bagian lain dokumen. Implementasinya mempertahankan seleksi saat ini dan membiarkan AppKit menangani input teks normal serta penggambaran kursor.
Kenali penyebab titik penyisipan tidak terlihat
Mulailah dari gejala yang terlihat. Pengguliran, fokus, seleksi, dan animasi kursor dapat menghasilkan tampilan yang serupa, tetapi masing-masing memerlukan respons berbeda.
| Yang Anda lihat | Yang perlu diperiksa | Tindakan yang sesuai |
|---|---|---|
| Posisi pengeditan Anda berada di luar area dokumen yang terlihat | Seleksi teks saat ini pada tampilan teks | Gulir rentang tersebut hingga terlihat |
| Mengeklik kontrol lain menghentikan pengetikan di editor | First responder pada jendela | Kembalikan fokus secara eksplisit saat diminta |
| Caret sulit dibedakan dari latar belakang | Warna caret dan latar belakang | Atur properti warna tampilan teks |
| Teks disorot alih-alih menampilkan satu caret | Apakah rentang yang dipilih memiliki panjang bukan nol | Pertahankan seleksi kecuali pengguna meminta perpindahan |
| Caret menghilang lalu muncul kembali saat editor aktif | Animasi kursor normal | Tangani kedipan secara terpisah dari pengguliran |
Apple mendokumentasikan scrollRangeToVisible(_:) sebagai cara menggulir tampilan gulir yang membungkus hingga karakter pertama dari rentang yang diberikan terlihat. API ini tidak menjamin rentang tersebut akan diposisikan di tengah atau seluruh seleksi panjang akan ditampilkan. Karena itu, API ini cocok untuk menampilkan posisi penyisipan tanpa menghitung sendiri persegi panjang glyph. Dokumentasi pengguliran Apple.
Fokus berada pada jendela yang menaungi tampilan. Apple secara eksplisit menginstruksikan pemanggil untuk menggunakan window.makeFirstResponder(textView), bukan memanggil becomeFirstResponder() pada tampilan teks secara langsung. Implementasi di bawah mengikuti jalur tersebut dan memeriksa objek mana yang benar-benar menjadi first responder. Dokumentasi NSTextView dari Apple, dokumentasi transisi first responder.
Buat editor lokal dengan perintah tampilkan
Buat proyek app macOS di Xcode menggunakan SwiftUI dan Swift. Untuk contoh ini, tetapkan target deployment ke macOS 13 atau lebih baru. Ganti titik masuk app yang dibuat otomatis dengan file pertama di bawah, lalu tambahkan file kedua ke target app yang sama. Kedua file ini membentuk app demonstrasi lengkap.
Contoh ini menggunakan teks sintetis dan menyimpan suntingan di tampilan AppKit selama masa hidup tampilan tersebut. Contoh ini tidak memiliki implementasi penyimpanan dokumen atau model teks eksternal. Menutup app akan membuang suntingan. Kode ini telah diperiksa berdasarkan dokumentasi yang diambil; kode tidak dikompilasi atau dijalankan di sini karena lingkungan yang tersedia adalah Linux dan tidak memiliki SDK AppKit.
Buat jendela SwiftUI
Tombol ini menaikkan nomor permintaan. Dengan begitu, wrapper menerima perintah khusus untuk diproses, alih-alih selalu mengembalikan fokus atau menggulir editor setiap kali SwiftUI diperbarui.
// 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)
}
}
Bungkus editor AppKit yang dapat digulir
NSViewRepresentable adalah jembatan Apple untuk mengelola tampilan AppKit dalam hierarki SwiftUI. Di sini, tampilan yang direpresentasikan adalah NSScrollView, dengan NSTextView sebagai tampilan dokumennya. Koordinator mengingat permintaan terakhir yang ditangani. Apple menyediakan factory tampilan teks yang dapat digulir untuk digunakan saat pembuatan. 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 dimulai dengan seleksi sepanjang nol di akhir teks contoh. Menekan Reveal insertion point akan memberi fokus pada editor dan menampilkan posisi tersebut. Tekanan berikutnya menggunakan seleksi yang sedang aktif, sehingga memindahkan caret ke tempat lain akan mengubah posisi yang ditampilkan oleh perintah itu. Perintah ini tidak mengganti teks atau memindahkan seleksi.
Posisi akhir menggunakan NSString.length, yang menghitung unit kode UTF-16. Tampilan UTF-16 Swift sesuai dengan API NSString yang menggunakan indeks; String.count di Swift menghitung karakter. Saat membuat rentang teks AppKit, gunakan representasi yang konsisten. Membaca selectedRange() yang sudah ada menghindari penghitungan offset untuk perintah tampilkan itu sendiri. NSString length, Swift String.UTF16View, Swift String.count.
Pertahankan status pengeditan selama pembaruan SwiftUI
Pilihan desain yang penting adalah membagi tanggung jawab antara pembuatan dan perintah. makeNSView mengisi editor satu kali. updateNSView merespons perubahan nomor permintaan dan membiarkan isi dokumen apa adanya. Ini mengikuti siklus hidup representable yang dijelaskan Apple untuk membuat dan memperbarui tampilan AppKit. SwiftUI juga mengontrol tata letak tampilan yang direpresentasikan, sehingga wrapper ini tidak menetapkan frame atau bounds tampilan gulir. Panduan siklus hidup representable dan tata letak dari Apple.
Hindari menetapkan textView.string = initialText pada setiap pembaruan dalam demonstrasi ini. Tindakan tersebut akan mengganti dokumen pengguna saat ini dengan teks contoh asli. Demikian pula, selalu menetapkan seleksi ke akhir dokumen akan membuat operasi tampilkan memindahkan posisi pengeditan.
Untuk editor yang didukung oleh status SwiftUI, tambahkan sinkronisasi dua arah yang dirancang dengan saksama: teruskan suntingan pengguna melalui koordinator dan tentukan kapan perubahan model yang masuk mengganti teks. Pertahankan seleksi yang valid saat penggantian tersebut dan tentukan cara menyelesaikan perubahan bersamaan. Ini tanggung jawab yang terpisah dari menampilkan caret; contoh ini sengaja mempertahankan satu pemilik konten yang dapat diedit. Apple mendokumentasikan koordinator sebagai mekanisme untuk meneruskan interaksi delegasi dan target-action ke SwiftUI. Panduan koordinator.
Pemeriksaan fokus juga penting. makeFirstResponder(_:) dapat gagal ketika responder yang ada menolak untuk melepas fokus. Apple juga menyatakan bahwa nilai balik yang berhasil dapat membuat jendela itu sendiri menjadi first responder jika responder yang diminta tidak menerima fokus. Memeriksa window.firstResponder === textView memverifikasi hasil sebenarnya sebelum menggulir. Nilai balik dan perilaku pengembalian first responder.
Jika editor belum terpasang ke jendela, contoh ini mengonsumsi permintaan tanpa melakukan apa pun. Tekan tombol lagi setelah editor terpasang. Untuk fitur fokus awal, rancang permintaan eksplisit yang mempertimbangkan keterpasangan, alih-alih berulang kali mengambil alih fokus saat pembaruan yang tidak terkait.
Atur warna tanpa mengganti penggambaran kursor
Contoh ini menetapkan .textColor untuk teks dan titik penyisipan, serta .textBackgroundColor untuk latar belakang editor. Keduanya adalah warna semantik terdokumentasi untuk teks dan area di belakangnya. insertionPointColor adalah properti terdokumentasi untuk mengatur warna caret. Periksa tampilannya pada setiap versi macOS dan mode tampilan yang didukung app Anda. Warna caret, warna teks, warna latar belakang teks.
Pengaturan warna tidak meminta fokus, menggulir dokumen, atau memilih mode tampilan yang tidak berkedip. Pisahkan tanggung jawab tersebut saat mendiagnosis caret yang tidak terlihat. Misalnya, mengubah warna tidak dapat menampilkan posisi yang masih berada di luar area tampilan gulir.
Contoh ini juga tidak mengakses layoutManager untuk menghitung geometri caret. Apple mendokumentasikan bahwa mengakses properti ini pada tampilan teks atau kontainer teks dapat mengalihkan tampilan ke mode kompatibilitas yang menggunakan NSLayoutManager pada macOS 12 dan versi lebih baru. Memanggil API pengguliran tingkat tinggi menghindari perubahan kompatibilitas TextKit ini hanya untuk menampilkan seleksi. Perilaku kompatibilitas TextKit.
Pahami batas caret yang selalu terlihat
Jika “tetap terlihat” berarti “tidak pernah berkedip,” perintah pengguliran tidak memenuhi persyaratan tersebut. Dokumentasi kursor modern Apple membedakan animasi otomatis dari mode tampilan yang selalu terlihat.
Pada macOS 14 dan versi lebih baru, NSTextInsertionIndicator menyediakan mode tampilan .automatic, .visible, dan .hidden. Apple mendokumentasikan .visible untuk menampilkan kursor terus-menerus dengan animasi dinonaktifkan, misalnya untuk menunjukkan tujuan seret. Tampilan teks AppKit standar secara otomatis menggunakan UI kursor sistem. Panduan penggunaan kursor teks sistem, NSTextInsertionIndicator.
Jalur kursor khusus yang didokumentasikan mengharuskan Anda membuat dan memosisikan indikator sendiri. Mode tampilannya mengontrol instans tersebut; panduan Apple tidak menyediakan pengaturan pada NSTextView untuk mengubah indikator yang dikelolanya secara internal menjadi .visible. Anggap jalur khusus sebagai pilihan arsitektur untuk UI teks khusus, termasuk tanggung jawab memperbarui geometri dan status aktif kursor. Ini bukan perubahan yang dapat langsung diterapkan pada wrapper ini.
Dua API yang tampak lebih lama tidak menyediakan pengaturan tersebut. shouldDrawInsertionPoint didokumentasikan sebagai hanya-baca. drawInsertionPoint(in:color:turnedOn:) menggambar atau menghapus gambar kursor, dan Apple menyatakan pemanggil tidak perlu memanggilnya secara langsung. Keduanya tidak didokumentasikan sebagai pengaturan agar kursor selalu terlihat. Status penggambaran titik penyisipan, Metode penggambaran titik penyisipan.
Untuk tugas editor biasa dalam artikel ini, pertahankan animasi kursor AppKit dan sediakan cara yang andal untuk kembali ke posisi saat ini. Implementasi kursor khusus memerlukan desain tersendiri dan, jika menggunakan NSTextInsertionIndicator, batas ketersediaan macOS 14.
Verifikasi perilaku di app Anda
Gunakan contoh ini untuk memisahkan pemeriksaan pengguliran, seleksi, fokus, dan tampilan. Berikut ini adalah perilaku yang diharapkan berdasarkan implementasi, bukan hasil eksekusi yang dilaporkan.
- Jalankan app dan tekan
Reveal insertion point. Editor seharusnya menerima fokus dan menampilkan posisi akhirnya. - Klik baris yang lebih awal untuk memindahkan caret. Gulir menjauh tanpa mengeklik posisi teks lain, lalu tekan tombol. Tombol seharusnya menampilkan posisi awal tersebut.
- Pilih beberapa kata lalu tekan tombol. Seleksi seharusnya tetap terpilih; perintah ini tidak mengubahnya menjadi caret.
- Ketik label unik seperti
orange triangle, lalu tekan tombol beberapa kali. Isi yang telah disunting seharusnya tetap utuh. - Ubah ukuran jendela dan ulangi operasi tampilkan. Perintah ini seharusnya tetap menggunakan rentang saat ini, bukan koordinat layar lama.
- Periksa tampilan terang dan gelap pada versi macOS yang didukung. Pastikan warna teks, latar belakang, dan caret tetap mudah dibaca secara bersamaan.
Jika pengguliran berhasil tetapi caret masih tampak berkedip, periksa apakah yang Anda lihat adalah kedipan normal atau seleksi teks yang aktif. Jika perintah tidak melakukan apa pun, periksa terlebih dahulu pemeriksaan keterpasangan jendela dan first responder. Sesuaikan perbaikan dengan langkah yang gagal: pertahankan teks, pulihkan fokus saat diminta, dan tampilkan rentang saat ini.
Sumber
- NSViewRepresentable — Apple
- NSTextView — Apple
- scrollableTextView() — Apple
- scrollRangeToVisible(_:) — Apple
- makeFirstResponder(_:) — Apple
- length — Apple
- String.UTF16View — Apple
- count — Apple
- insertionPointColor — Apple
- textColor — Apple
- textBackgroundColor — Apple
- Adopting the system text cursor in custom text views — Apple
- NSTextInsertionIndicator — Apple
- shouldDrawInsertionPoint — Apple
- drawInsertionPoint(in:color:turnedOn:) — Apple