Untuk menghantar perintah menu SwiftUI kepada NSTextView aktif, tentukan first responder bagi tetingkap utama apabila perintah dijalankan, kemudian hantar tindakan kepada paparan itu. Tutorial ini membina contoh dengan dua editor; perintah Select First Line hanya mempengaruhi editor yang difokuskan.
Tugas ini ialah tindakan menu tersuai dalam aplikasi macOS anda sendiri. Contoh ini menggunakan teks sintetik, subkelas NSTextView milik aplikasi, serta API SwiftUI dan AppKit awam. Keputusan reka bentuk utamanya ialah sama ada perintah patut mengikut rantaian responder AppKit atau memerlukan satu editor aktif yang khusus.
Tentukan maksud “paparan teks aktif”
Dalam contoh ini, editor aktif ialah tika SampleTextView yang kini menjadi first responder bagi tetingkap utama. Apple mentakrifkan tetingkap utama sebagai tetingkap yang menerima peristiwa papan kekunci; firstResponder mengenal pasti responder yang biasanya menerima peristiwa dan tindakan terlebih dahulu. Membaca sifat ini pada masa perintah dipanggil memberikan destinasi semasanya. Dokumentasi Apple tentang keyWindow, dokumentasi Apple tentang firstResponder.
AppKit turut menyokong tindakan tanpa sasaran: hantar nil sebagai sasaran kepada NSApplication.sendAction(_:to:from:), dan aplikasi akan mencari responder yang melaksanakan pemilih tersebut. Carian itu bermula pada first responder tetingkap utama, tetapi boleh diteruskan melalui wakil dan tetingkap utama sebelum sampai kepada pengendali peringkat aplikasi. Oleh itu, tindakan tanpa sasaran tidak bermaksud “laksanakan hanya pada paparan teks aktif dalam tetingkap utama ini.” Dokumentasi Apple tentang penghalaan tindakan.
| Tingkah laku yang diperlukan | Pilihan penghalaan | Akibat |
|---|---|---|
| Biarkan AppKit mencari pengendali yang sesuai | Hantar tindakan dengan to: nil |
Sandaran rantaian responder biasa digunakan. |
| Hanya mempengaruhi editor aktif milik aplikasi | Tentukan dan semak jenis first responder, kemudian sasarkan secara jelas kepadanya | Perintah tidak diteruskan jika editor itu tidak aktif. |
| Beroperasi pada editor tertentu tanpa mengira fokus | Berikan editor itu sebagai sasaran | Interaksi ini berbeza daripada perintah untuk editor aktif. |
Pelaksanaan di bawah memilih tingkah laku kedua. Semakan subkelas tersuai juga menghalang editor medan teks yang tidak berkaitan daripada menjadi destinasi hanya kerana ia ialah paparan teks.
Bina contoh SwiftUI dan AppKit yang lengkap
Cipta projek aplikasi macOS dengan SwiftUI dan gunakan sasaran penggunaan macOS 13 atau lebih baharu untuk contoh ini. Gantikan titik masuk aplikasi yang dijana dengan fail Swift tunggal berikut; buang sebarang pengisytiharan @main yang lain. Program ini menyediakan kedua-dua editor, teks awalnya, menu dan pengendali tindakan.
NSViewRepresentable SwiftUI ialah jambatan yang disokong untuk membenamkan paparan AppKit. Perintah scene menyediakan kandungan menu, manakala CommandMenu mencipta menu khusus aplikasi dalam bar menu macOS. Dokumentasi Apple tentang NSViewRepresentable, perintah scene, menu perintah.
import SwiftUI
import AppKit
import Foundation
@main
@MainActor
struct RoutingDemoApp: App {
var body: some Scene {
WindowGroup("Routing Demo") {
ContentView()
}
.commands {
CommandMenu("Sample Actions") {
Button("Select First Line") {
Self.routeSelectFirstLine()
}
}
}
}
private static func routeSelectFirstLine() {
let application = NSApplication.shared
guard let editor = application.keyWindow?.firstResponder
as? SampleTextView else {
return
}
_ = application.sendAction(
#selector(SampleTextView.selectFirstSampleLine(_:)),
to: editor,
from: nil
)
}
}
@MainActor
struct ContentView: View {
@State private var windowLabel = "Sample Window"
var body: some View {
VStack(alignment: .leading, spacing: 12) {
TextField("Window label", text: $windowLabel)
HStack(spacing: 12) {
SampleEditor(initialText: "Sample Alpha\nAlpha details")
SampleEditor(initialText: "Sample Beta\nBeta details")
}
Text("Click an editor, then use Sample Actions.")
.font(.caption)
}
.padding()
.frame(minWidth: 640, minHeight: 320)
}
}
@MainActor
final class SampleTextView: NSTextView {
@objc func selectFirstSampleLine(_ sender: Any?) {
guard isSelectable, !hasMarkedText() else { return }
let contents = string as NSString
guard contents.length > 0 else { return }
let firstLine = contents.lineRange(
for: NSRange(location: 0, length: 0)
)
setSelectedRange(firstLine)
scrollRangeToVisible(firstLine)
}
}
@MainActor
struct SampleEditor: NSViewRepresentable {
let initialText: String
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSScrollView()
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = false
scrollView.borderType = .bezelBorder
let editor = SampleTextView(
frame: NSRect(x: 0, y: 0, width: 300, height: 240)
)
editor.isEditable = true
editor.isSelectable = true
editor.isRichText = false
editor.allowsUndo = true
editor.font = NSFont.monospacedSystemFont(
ofSize: 14, weight: .regular
)
editor.minSize = .zero
editor.maxSize = NSSize(
width: CGFloat.greatestFiniteMagnitude,
height: CGFloat.greatestFiniteMagnitude
)
editor.isVerticallyResizable = true
editor.isHorizontallyResizable = false
editor.autoresizingMask = [.width]
editor.textContainer?.containerSize = NSSize(
width: 300,
height: CGFloat.greatestFiniteMagnitude
)
editor.textContainer?.widthTracksTextView = true
editor.string = initialText
scrollView.documentView = editor
return scrollView
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
// AppKit owns the editable contents in this example.
}
}
Ini ialah contoh aplikasi lengkap dengan kandungan editor dalam memori. Contoh ini telah diteliti berdasarkan dokumentasi API yang diperoleh; ia tidak dikompil atau dijalankan di sini kerana persekitaran yang tersedia ialah Linux dan tidak mempunyai toolchain Swift.
Sebab pemilih mencapai editor yang dimaksudkan
Fungsi penghalaan mendapatkan first responder sejurus sebelum penghantaran. Fungsi itu hanya menerima SampleTextView dan menetapkan objek tepat itu sebagai sasaran tindakan. Maka, menu tidak menyimpan rujukan kepada editor yang diklik sebelum ini dan tidak mencari tetingkap lain jika semakan jenis gagal.
Kaedah tindakan menggunakan @objc kerana penghantaran sasaran-tindakan menggunakan pemilih Objective-C. Ungkapan #selector merujuk kepada kaedah yang diisytiharkan, bukannya rentetan pemilih yang ditulis sendiri. Apple mendokumentasikan gabungan ini untuk memanggil kaedah Swift melalui API sasaran-tindakan Objective-C. Menggunakan Ciri Runtime Objective-C dalam Swift.
Nilai yang dikembalikan oleh sendAction menunjukkan sama ada mesej dihantar. Nilai itu tidak menunjukkan sama ada pengendali mengubah pemilihan: pengendali boleh menerima mesej dan terus keluar jika kandungannya kosong atau terdapat teks bertanda. Contoh ini sengaja mengabaikan hasil penghantaran kerana destinasi dan kaedahnya sudah diketahui. Dokumentasi Apple tentang nilai pulangan sendAction.
Perkara yang sebenarnya dipilih oleh perintah pemilihan
Pengendali meminta NSString.lineRange(for:) untuk baris logik yang mengandungi permulaan rentetan. Julat itu merangkumi penamat barisnya, jika ada. Oleh itu, ia memilih baris pertama yang dipisahkan baris baharu, walaupun baris itu melilit merentasi beberapa baris paparan. Dokumentasi Apple tentang julat baris.
Bagi teks yang dibekalkan, editor kiri memilih Sample Alpha dan baris baharu selepasnya; editor kanan memilih Sample Beta dan baris baharu selepasnya. Perintah ini menggantikan pemilihan sebelumnya. Ia tidak mengubah teks.
setSelectedRange(_:) menetapkan pemilihan, manakala scrollRangeToVisible(_:) menatal supaya aksara pertamanya kelihatan. Semakan kandungan kosong mengelakkan percubaan operasi pemilihan tanpa sebarang teks. Semakan teks bertanda ialah dasar contoh yang jelas: tangguhkan tindakan pemilihan tersuai ini semasa penggubahan input sedang aktif. Julat pemilihan, menatal julat supaya kelihatan, teks bertanda.
Asingkan penciptaan paparan, penyuntingan dan fokus
Representable ini mencipta NSScrollView dan memasang paparan teks tersuai sebagai paparan dokumennya. Editor berkembang secara menegak, manakala bekas teksnya menjejaki lebarnya. Susunan ini mengikut susunan paparan teks boleh tatal yang didokumentasikan oleh Apple. Susunan itu muncul dalam panduan AppKit Apple yang diarkibkan; jambatan SwiftUI dan API tindakan yang dirujuk di sini menggunakan dokumentasi semasa. Meletakkan Objek NSTextView dalam NSScrollView.
SwiftUI mengawal reka letak paparan tatal yang diwakili. Kod ini mengkonfigurasi geometri awal paparan dokumen bersarang dan tingkah laku saiz semula AppKit; kod ini tidak berulang kali menetapkan bingkai paparan tatal yang diwakili. Apple secara khusus mengingatkan supaya tidak mengubah sifat reka letak yang diuruskan SwiftUI untuk representable. Panduan Apple tentang reka letak representable.
Contoh ini menetapkan initialText hanya semasa penciptaan. Kaedah kemas kini yang kosong sengaja membiarkan AppKit mengawal penaipan seterusnya. Jika aplikasi anda memerlukan model dokumen SwiftUI, tambahkan penyelaras dan penyegerakan berasaskan wakil, dengan dasar yang jelas untuk perubahan model yang tiba semasa penyuntingan. Menetapkan semula rentetan awal pada setiap kemas kini akan menggantikan kandungan semasa pengguna. Apple menerangkan penyelaras sebagai jambatan untuk menghantar interaksi AppKit ke dalam SwiftUI. Panduan Apple tentang penyelarasan representable.
Mengklik editor menetapkan fokus untuk panduan ini. Jika aplikasi anda perlu mengalihkan fokus secara programatik, gunakan makeFirstResponder(_:) bagi tetingkap yang mengandungi paparan itu selepas paparan tersebut menjadi sebahagian daripada tetingkap berkenaan. Apple secara jelas menyatakan supaya tidak memanggil becomeFirstResponder() pada paparan teks secara langsung. Elakkan meminta fokus pada setiap kemas kini SwiftUI kerana tindakan itu akan berulang kali mengalihkan destinasi penyuntingan pengguna. Panduan Apple tentang fokus paparan teks.
Kekalkan konfigurasi TextKit sedia ada
Penghalaan menu tidak memerlukan perubahan pada enjin reka letak teks. Contoh ini menggunakan NSTextView(frame:); Apple mendokumentasikan pemula itu sebagai menggunakan NSLayoutManager secara lalai. Pembinaan eksplisit dengan NSTextLayoutManager disokong mulai macOS 12. Dokumentasi Apple tentang pemulaan paparan teks.
Untuk editor TextKit 2 sedia ada, kekalkan pembinaannya dan tambahkan tindakan pada subkelasnya. Elakkan membaca layoutManager semata-mata untuk mencari atau menghala ke editor: Apple mendokumentasikan bahawa mengakses sifat itu pada paparan teks TextKit 2 boleh menukarkannya kepada mod keserasian. Kod penghalaan hanya memerlukan responder dan kaedah tindakan. Dokumentasi Apple tentang mod keserasian NSTextView.
Sahkan penghalaan dan kendalikan perintah yang tidak tersedia
Gunakan urutan ini untuk menyemak tingkah laku yang dimaksudkan pada macOS. Ini ialah hasil yang dijangka berdasarkan pelaksanaan, bukannya hasil pelaksanaan yang dilaporkan.
- Klik dalam editor kiri, kemudian pilih
Sample Actions→Select First Line. Baris logik pertama dalam editor kiri sepatutnya dipilih. - Klik dalam editor kanan dan panggil perintah yang sama. Hanya pemilihan editor kanan sepatutnya berubah.
- Klik dalam
Window labeldan panggil perintah itu. Kedua-dua editor tersuai sepatutnya kekal tanpa perubahan kerana first responder gagal dalam semakan subkelas. - Padam semua kandungan daripada editor dan panggil perintah itu di situ. Pengawal kandungan kosong sepatutnya keluar tanpa mengubah teks.
- Tambahkan beberapa baris, tatal ke bawah dan panggil perintah itu. Baris pertama sepatutnya dipilih dan ditatal supaya kelihatan.
Perintah ini kekal diaktifkan dalam contoh ringkas ini, termasuk apabila fokus berada di tempat lain. Pengawal pada masa perintah dipanggil menentukan sama ada perintah itu bertindak. Membaca keyWindow?.firstResponder dalam ungkapan SwiftUI .disabled sahaja tidak menyediakan mekanisme untuk mengemas kini ungkapan itu setiap kali fokus AppKit berubah.
Bagi antara muka SwiftUI yang lebih besar, terbitkan keupayaan perintah yang sesuai melalui keadaan yang peka kepada fokus, perhatikannya dengan @FocusedValue, dan tentukan syarat .disabled butang berdasarkan keadaan tersebut. Jambatan AppKit mesti memastikan keupayaan itu selaras dengan fokus editor sebenar. Kekalkan juga semakan pada masa perintah dipanggil. Apple mendokumentasikan FocusedValue sebagai memerhatikan nilai daripada paparan yang difokuskan atau leluhurnya. Nilai berfokus, menyahdayakan interaksi.
Jika anda sebaliknya membina item menu AppKit yang sasarannya ialah editor, validateMenuItem(_:) menyediakan cangkuk pengesahan AppKit. Menambahkan kaedah itu pada subkelas ini sahaja tidak akan mengesahkan butang SwiftUI: tindakannya ialah penutupan yang kemudiannya menghantar tindakan lain. Apple mensyaratkan objek pengesahan ialah sasaran item menu. Dokumentasi Apple tentang pengesahan item menu.
Pilih dasar sasaran sebelum menambahkan lebih banyak perintah. Gunakan tindakan tanpa sasaran apabila sandaran rantaian responder ialah tingkah laku yang dikehendaki; tentukan dan sasarkan first responder apabila operasi itu khusus untuk editor aktif. Pilihan itu memastikan menu yang sama kekal mudah dijangka apabila pengguna beralih antara paparan teks dan kawalan lain.
Sumber
sendAction(_:to:from:)— ApplekeyWindow— ApplefirstResponder— AppleNSViewRepresentable— Applecommands(content:)— AppleCommandMenu— Apple- Menggunakan Ciri Runtime Objective-C dalam Swift — Apple
lineRange(for:)— ApplesetSelectedRange(_:)— ApplescrollRangeToVisible(_:)— ApplehasMarkedText()— Apple- Meletakkan Objek NSTextView dalam NSScrollView — Apple
NSTextView— Appleinit(frame:textContainer:)— AppleFocusedValue— Appledisabled(_:)— ApplevalidateMenuItem(_:)— Apple