若要让以编程方式创建的 NSTextView 接受键入,请启用编辑,将其附加到窗口,然后调用该窗口的 makeFirstResponder(_:)。窗口还必须能够成为键窗口。下面是一个完整示例和一套有针对性的故障排查步骤。

键入生效前必须满足哪些条件?

本指南介绍一项任务:创建一个小型 Mac 应用窗口,让用户能够键入和编辑多行文本。指南直接使用 AppKit,然后说明在 SwiftUI 中嵌入编辑器时相应的生命周期选择。

有三个独立条件需要满足:文本视图必须允许编辑,窗口必须接收键盘事件,而文本视图必须是该窗口中指定的第一响应者。Apple 将应用的 keyWindow 定义为当前接收键盘事件的窗口;仅仅显示窗口,并不能确定哪个视图应当编辑文本。Apple:keyWindow

设置 isEditable = true 可允许编辑,也会使文本视图可供选择。Apple 文档指出,文本视图默认可编辑,但显式设置这一属性能让示例的配置更加清晰。仅启用编辑并不会选定键盘输入目标。Apple:isEditable

若要设置焦点,请使用其所属窗口的 makeFirstResponder(textView)。Apple 明确要求开发者使用此方法,而不是直接调用文本视图的 becomeFirstResponder()。这样窗口便能协调响应者之间的切换。Apple:NSTextView

在 Swift 中构建完整的可编辑窗口

下面的独立程序面向 macOS 14 或更高版本,使用当前 Swift 工具链和 macOS SDK。窗口标题为 Local Editor,初始文本为 Sample note。文本保存在内存中;此示例不实现保存功能。

将完整程序保存为 EditorDemo.swift。请将其作为独立可执行程序使用,而不是把第二个入口点添加到现有应用中。Swift 只允许一个程序入口点;下面的 @main 类型提供该入口点。Swift 编程语言:属性

import AppKit

@MainActor
final class EditorDelegate: NSObject, NSApplicationDelegate {
    private var editorWindow: NSWindow?

    func applicationDidFinishLaunching(_ notification: Notification) {
        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 640, height: 420),
            styleMask: [.titled, .closable, .miniaturizable, .resizable],
            backing: .buffered,
            defer: false
        )
        window.title = "Local Editor"
        window.isReleasedWhenClosed = false

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

        textView.isEditable = true
        textView.isSelectable = true
        textView.isRichText = false
        textView.allowsUndo = true
        textView.font = NSFont.systemFont(ofSize: 16)
        textView.textContainerInset = NSSize(width: 12, height: 12)
        textView.string = "Sample note\n"
        textView.setSelectedRange(
            NSRange(location: (textView.string as NSString).length, length: 0)
        )

        scrollView.frame = NSRect(x: 0, y: 0, width: 640, height: 420)
        scrollView.autoresizingMask = [.width, .height]
        window.contentView = scrollView
        window.initialFirstResponder = textView
        editorWindow = window

        let mainMenu = NSMenu()
        let appMenuItem = NSMenuItem()
        mainMenu.addItem(appMenuItem)
        let appMenu = NSMenu()
        appMenu.addItem(NSMenuItem(
            title: "Quit Local Editor",
            action: #selector(NSApplication.terminate(_:)),
            keyEquivalent: "q"
        ))
        appMenuItem.submenu = appMenu
        NSApp.mainMenu = mainMenu

        window.center()
        window.makeKeyAndOrderFront(nil)
        NSApp.activate()

        guard textView.window === window,
              textView.acceptsFirstResponder else {
            print("Editor is not ready to receive focus")
            return
        }

        let accepted = window.makeFirstResponder(textView)
        print("Focus request returned:", accepted)
        print("Editor is first responder:", window.firstResponder === textView)
        print("Window is key:", window.isKeyWindow)
    }

    func applicationShouldTerminateAfterLastWindowClosed(
        _ sender: NSApplication
    ) -> Bool {
        true
    }
}

@main
@MainActor
enum EditorDemo {
    static func main() {
        let application = NSApplication.shared
        application.setActivationPolicy(.regular)
        let delegate = EditorDelegate()
        application.delegate = delegate
        withExtendedLifetime(delegate) {
            application.run()
        }
    }
}

在安装了 Xcode 或命令行工具的 Mac 上,进入文件所在目录并编译、运行:

xcrun swiftc -parse-as-library \
  -target "$(uname -m)-apple-macos14.0" \
  -framework AppKit EditorDemo.swift -o EditorDemo
./EditorDemo

本示例已根据检索到的文档进行检查,但由于当前研究环境运行 Linux 且没有 Swift 编译器,因此无法编译或运行。请在 macOS 上按照下面的步骤检查窗口行为。

此设置如何解决键盘输入问题

便捷方法 scrollableTextView() 会返回一个 NSScrollView;代码通过 documentView 获取其中的文本视图。将文本编辑器放在滚动视图中,可为这个小型示例的多行内容提供一个简单明了的容器。此工厂方法从 macOS 10.14 起可用。Apple:scrollableTextView()

代码会先附加该容器,再请求焦点。它还设置了 initialFirstResponder,Apple 将其定义为窗口首次出现时选定的视图。随后调用 makeFirstResponder(_:) 显式请求将编辑器设为响应者,并提供可供检查的结果。初始焦点和后续焦点请求对应窗口生命周期中的不同阶段。Apple:initialFirstResponder,Apple:makeFirstResponder(_:)

窗口使用带标题栏的样式,因此能够成为键窗口。makeKeyAndOrderFront(nil) 同时请求显示窗口并使其成为键窗口。Apple 将其与 orderFront(_:) 区分开来;后者只改变窗口顺序,不会改变键窗口。Apple:canBecomeKey,Apple:makeKeyAndOrderFront(_:)

调用 activate() 是此程序要求 macOS 14 或更高版本的原因。Apple 将激活描述为一种请求,而非保证。如果窗口出现后仍无法键入,请点击窗口来激活应用,然后再次检查。不要把立即输出的诊断信息视为激活已完成的保证。Apple:activate()

入口点和代理都标记为 @MainActor;Apple 将其执行器定义为等同于主分派队列。程序还通过 run() 启动 AppKit 的事件循环,使 AppKit 能在设置完成后分派用户事件。Apple:MainActor,Apple:run()

排查可见但仍无法接受键入的编辑器

请按顺序执行以下步骤。它们分别检查视图附加状态、窗口资格、响应者选择和可编辑状态,因此每项检查的结果都会指向具体的下一步操作。

检查项 结果含义 下一步操作
textView.window === window 编辑器属于指定窗口。 如果为 false,请先完成视图层级的附加,再请求焦点。
window.canBecomeKey 窗口有资格成为键窗口。 如果为 false,请检查窗口样式或自定义子类。
window.isKeyWindow 此窗口当前是应用中的键窗口。 激活应用并选中指定窗口,然后重新检查。
textView.acceptsFirstResponder 编辑器接受焦点请求。 如果为 false,请检查配置和子类重写。
window.firstResponder === textView 这个编辑器确切地拥有第一响应者状态。 如果为 false,请检查焦点切换和当前响应者。
textView.isEditable 已启用用户编辑。 如果需要编辑,请将其设为 true。

响应者检查尤其有用,因为 makeFirstResponder(_:) 有一个文档中提到的细节:如果请求的响应者拒绝成为第一响应者,而窗口本身成为第一响应者,该方法可能返回 true。Apple 还指出,在显式发出请求前应检查 acceptsFirstResponder。因此,请同时检查返回值和最终的 firstResponder 身份。Apple:makeFirstResponder(_:)

返回 false 可能表示现有第一响应者拒绝放弃响应者状态。请检查该响应者的行为,以及是否涉及编辑代理。例如,Apple 文档指出,文本视图的代理可以通过 textShouldEndEditing(_:) 拒绝结束编辑。在未解决拒绝原因的情况下重复相同的焦点请求,并不能解决问题。Apple:makeFirstResponder(_:),Apple:NSTextView

进行基本的手动检查时,输入 Blue square,按 Return,再输入 Green circle,然后使用方向键和 Delete 编辑文本。调整窗口大小并继续输入。这些是建议的验证步骤,不是已报告的测试结果。完成后关闭窗口;示例程序随即终止。

无边框窗口会有什么变化?

可见的无边框窗口仍可能无法成为键窗口。Apple 文档指出,不带标题栏样式的普通 NSWindow 默认不能成为键窗口。受支持的自定义方式是在 NSWindow 子类中重写 canBecomeKey,并在该窗口需要接收键盘输入时返回 true。Apple:canBecomeKey

先使用带标题栏的示例,确认文本输入正常。若界面之后需要无边框窗口,请修改窗口实现,同时保留相同的视图附加、可编辑状态和响应者检查。更改窗口外观并不能取代这些要求。

如何让同一编辑器在 SwiftUI 中工作?

使用 NSViewRepresentable 将 AppKit 文本视图集成到 SwiftUI 层级中。Apple 将创建和初始配置分配给 makeNSView(context:),将持续状态更新分配给 updateNSView(_:context:),并将与代理或 target-action 消息的协调交给协调器。Apple:NSViewRepresentable

对于这个编辑器,一种实用设计是将滚动视图作为所表示的视图返回,并在协调器或自定义容器中保留对文档文本视图的引用。在创建时配置可编辑状态。如果外围 SwiftUI 界面需要绑定,请通过代理转发用户的文本更改。

将视图附加视为与创建分开的事件。请求焦点前,请检查编辑器的 window 是否非 nil。AppKit 子类可通过 viewDidMoveToWindow() 观察层级变化;Apple 文档指出,此处的 window 为 nil,表示该视图当前不属于任何窗口。这为实现可感知附加状态的焦点请求提供了有文档依据的生命周期钩子。Apple:viewDidMoveToWindow()

作为实现策略,可在指定界面首次呈现时请求一次初始焦点,或响应诸如 Focus Editor 这样的显式操作。避免在每次 SwiftUI 更新时无条件调用 makeFirstResponder(_:):这种做法会在人选择其他控件后反复将焦点移回编辑器。请求焦点时应检查所属窗口,而不要假定先前保存的窗口仍然合适。

还应避免在每次更新时重置编辑器的字符串。对于基于绑定的包装器,应将传入状态与当前文本进行比较,并在替换内容前考虑标记文本和选区。独立示例只初始化一次字符串;需要同步的编辑器则需要明确的更新策略。

让 SwiftUI 调整所表示的根视图大小。Apple 警告,直接更改该视图的 frame 或 bounds 会与 SwiftUI 的布局管理相冲突。因此,独立 AppKit 程序中的直接 frame 设置,不应照搬到由 SwiftUI 管理的根视图上。Apple:NSViewRepresentable

接受键入是否需要自定义 TextKit 栈?

对于此任务,请使用 NSTextView 现有的编辑行为。Apple 将其描述为文本系统的前端,负责绘制文本并处理选区、修改、输入管理和按键绑定。普通键入无需重建这些功能。Apple:NSTextView

TextKit 引擎选择与键盘焦点是两个独立的决定。Apple 文档指出,init(frame:) 默认使用 NSLayoutManager,而手动组装的文本视图可在 macOS 12 或更高版本中通过指定的初始化方法使用 NSTextLayoutManager。应根据布局需求选择引擎;切换引擎不能替代将编辑器设为第一响应者。Apple:init(frame:textContainer:)

有一项诊断需要谨慎对待:读取 layoutManager 不一定只是被动检查。Apple 文档指出,对使用较新系统的文本视图或文本容器访问该属性,可能会触发使用 NSLayoutManager 的兼容模式。示例不会查询此属性,也不假设滚动视图便捷工厂会选择哪个引擎。Apple:NSTextView

正常工作的结果很明确:指定的应用窗口处于活动状态并成为键窗口,其可编辑文本视图是第一响应者,键入会改变显示的文本。先用这个小窗口确认该行为,然后在添加 SwiftUI 集成、自定义布局或其他控件时保留相同的检查。

来源