SwiftUI中为TextField/TextEditor添加ContextMenu的macOS实现问题
在macOS SwiftUI中为TextEditor/TextField添加自定义右键菜单
macOS上的SwiftUI TextEditor 和 TextField 自带系统默认上下文菜单,直接使用.contextMenu修饰符会被系统菜单覆盖,因此需要通过包装底层AppKit控件的方式实现自定义右键菜单,同时支持修改选中文本、插入自定义内容的需求。
一、自定义TextEditor实现
通过NSViewRepresentable包装NSTextView,重写菜单逻辑并绑定文本:
import SwiftUI struct CustomTextEditor: NSViewRepresentable { @Binding var text: String func makeNSView(context: Context) -> NSTextView { let textView = NSTextView() textView.delegate = context.coordinator textView.string = text return textView } func updateNSView(_ nsView: NSTextView, context: Context) { if nsView.string != text { nsView.string = text } } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, NSTextViewDelegate { var parent: CustomTextEditor init(_ parent: CustomTextEditor) { self.parent = parent } func textDidChange(_ notification: Notification) { guard let textView = notification.object as? NSTextView else { return } parent.text = textView.string } } } // 扩展NSTextView重写菜单逻辑 extension NSTextView { open override func menu(for event: NSEvent) -> NSMenu? { let menu = super.menu(for: event) ?? NSMenu() // 添加分隔线 menu.addItem(NSMenuItem.separator()) // 自定义按钮:修改选中文本为大写 let modifyItem = NSMenuItem(title: "转为大写", action: #selector(modifySelectedText), keyEquivalent: "") modifyItem.target = self menu.addItem(modifyItem) // 自定义按钮:插入预设剪辑内容 let pasteClipItem = NSMenuItem(title: "插入剪辑内容", action: #selector(pasteCustomClip), keyEquivalent: "") pasteClipItem.target = self menu.addItem(pasteClipItem) return menu } @objc private func modifySelectedText() { guard let selectedRange = selectedRange, selectedRange.length > 0 else { return } let selectedText = string.substring(with: selectedRange) replaceCharacters(in: selectedRange, with: selectedText.uppercased()) } @objc private func pasteCustomClip() { let clipContent = "自定义剪辑内容" replaceCharacters(in: selectedRange, with: clipContent) } }
使用方式
在SwiftUI视图中直接调用:
struct ContentView: View { @State private var information = "测试文本" var body: some View { CustomTextEditor(text: $information) .frame(height: 200) .padding() } }
二、自定义TextField实现
类似地,包装NSTextField实现自定义菜单:
import SwiftUI struct CustomTextField: NSViewRepresentable { @Binding var text: String var placeholder: String func makeNSView(context: Context) -> NSTextField { let textField = NSTextField() textField.placeholderString = placeholder textField.delegate = context.coordinator textField.stringValue = text return textField } func updateNSView(_ nsView: NSTextField, context: Context) { if nsView.stringValue != text { nsView.stringValue = text } } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, NSTextFieldDelegate { var parent: CustomTextField init(_ parent: CustomTextField) { self.parent = parent } func controlTextDidChange(_ obj: Notification) { guard let textField = obj.object as? NSTextField else { return } parent.text = textField.stringValue } } } extension NSTextField { open override func menu(for event: NSEvent) -> NSMenu? { let menu = super.menu(for: event) ?? NSMenu() menu.addItem(NSMenuItem.separator()) let modifyItem = NSMenuItem(title: "转为小写", action: #selector(modifySelectedText), keyEquivalent: "") modifyItem.target = self menu.addItem(modifyItem) return menu } @objc private func modifySelectedText() { guard let editor = currentEditor(), let selectedRange = editor.selectedRange, selectedRange.length > 0 else { return } let selectedText = stringValue.substring(with: selectedRange) editor.replaceCharacters(in: selectedRange, with: selectedText.lowercased()) stringValue = editor.string } }
使用方式
struct ContentView: View { @State private var inputText = "" var body: some View { CustomTextField(text: $inputText, placeholder: "输入文本") .frame(width: 300, height: 40) .padding() } }
关键说明
- 重写
menu(for:)方法时,可选择保留系统菜单并添加自定义选项,也可以直接创建新NSMenu完全替换系统菜单。 - 文本操作通过
selectedRange获取选中区域,用replaceCharacters(in:with:)修改内容,确保同步到SwiftUI绑定值。 NSTextField需通过currentEditor()获取内部文本编辑器实例来操作选中内容。
内容的提问来源于stack exchange,提问作者mihema
相关产品推荐
相关产品推荐

