You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 03:01:06