To make a programmatically created NSTextView accept typing, enable editing, attach it to your window, and call that window’s makeFirstResponder(_:). The window must also be able to become key. Here is a complete example and a focused troubleshooting sequence.
What must be true before typing works?
This guide addresses one task: creating a small Mac app window where a person can type and edit multiline text. It uses AppKit directly, then explains the corresponding lifecycle choices when embedding the editor in SwiftUI.
Three separate conditions matter. The text view must allow editing, the window must receive keyboard events, and the text view must be the intended first responder within that window. Apple identifies the application’s keyWindow as the window currently receiving keyboard events; merely displaying a window does not establish which view should edit text. Apple: keyWindow
Setting isEditable = true permits editing and also makes the text view selectable. Apple documents that text views are editable by default, but setting this explicitly makes the example’s configuration clear. Editability alone does not select the keyboard destination. Apple: isEditable
For focus, use the containing window’s makeFirstResponder(textView). Apple explicitly instructs developers to use this method rather than calling the text view’s becomeFirstResponder() directly. This lets the window coordinate the transition between responders. Apple: NSTextView
Build a complete editable window in Swift
The following standalone program targets macOS 14 or later and uses a current Swift toolchain with the macOS SDK. Its synthetic window title is Local Editor, and its initial text is Sample note. The text remains in memory; this example does not implement saving.
Save the complete program as EditorDemo.swift. Use it as a standalone executable rather than adding a second entry point to an existing app. Swift permits only one program entry point; the @main type below supplies it. The Swift Programming Language: Attributes
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()
}
}
}
On a Mac with Xcode or the Command Line Tools installed, compile and launch it from the directory containing the file:
xcrun swiftc -parse-as-library \
-target "$(uname -m)-apple-macos14.0" \
-framework AppKit EditorDemo.swift -o EditorDemo
./EditorDemo
The example was inspected against the retrieved documentation but could not be compiled or exercised in this research environment, which runs Linux and has no Swift compiler. Verify the window behavior on macOS with the checks below.
Why this setup addresses keyboard input
The convenience method scrollableTextView() returns an NSScrollView; the code obtains its text view through documentView. Keeping the text editor inside a scroll view gives this small example a straightforward container for multiline content. The factory is available from macOS 10.14. Apple: scrollableTextView()
The code attaches that container before requesting focus. It also sets initialFirstResponder, which Apple defines as the view selected when the window first appears. The later makeFirstResponder(_:) call explicitly requests the editor and provides a result to inspect. Initial focus and a later focus request serve different points in the window’s lifecycle. Apple: initialFirstResponder, Apple: makeFirstResponder(_:)
The window uses a titled style so it can become key. makeKeyAndOrderFront(nil) requests both window presentation and key status. Apple distinguishes this from orderFront(_:), which changes ordering without changing the key window. Apple: canBecomeKey, Apple: makeKeyAndOrderFront(_:)
The call to activate() is why this particular program requires macOS 14 or later. Apple describes activation as a request, not a guarantee. If the window appears without receiving typing, activate the app by clicking its window and check again. Do not interpret an immediate diagnostic print as a promise that activation has completed. Apple: activate()
The entry point and delegate are marked @MainActor, whose executor Apple defines as equivalent to the main dispatch queue. The program also starts AppKit’s event loop with run(), allowing AppKit to dispatch user events after setup finishes. Apple: MainActor, Apple: run()
Diagnose a visible editor that still will not accept typing
Work through the following sequence in order. It separates attachment, window eligibility, responder selection, and editability, so each observation points to a specific next action.
| Check | What the result means | Next action |
|---|---|---|
textView.window === window |
The editor belongs to the intended window. | If false, finish attaching the view hierarchy before requesting focus. |
window.canBecomeKey |
The window is eligible to receive key status. | If false, inspect its style or custom subclass. |
window.isKeyWindow |
This window is currently key within the app. | Activate the app and select the intended window, then recheck. |
textView.acceptsFirstResponder |
The editor accepts a focus request. | If false, inspect configuration and subclass overrides. |
window.firstResponder === textView |
This exact editor owns first-responder status. | If false, inspect the focus transition and current responder. |
textView.isEditable |
User edits are enabled. | Set it to true if editing is intended. |
The responder check is especially useful because makeFirstResponder(_:) has a documented subtlety: it can return true when the requested responder refuses first-responder status and the window becomes first responder instead. Apple also says to check acceptsFirstResponder before explicitly making the request. Therefore, inspect both the return value and the resulting firstResponder identity. Apple: makeFirstResponder(_:)
A false result can mean the existing first responder refused to resign. Review that responder’s behavior and any editing delegate involved. For example, Apple documents that a text view’s delegate can reject ending editing through textShouldEndEditing(_:). Repeating the same focus request without resolving the refusal does not address its cause. Apple: makeFirstResponder(_:), Apple: NSTextView
For a basic manual check, type Blue square, press Return, type Green circle, then use the arrow keys and Delete to edit the text. Resize the window and continue typing. These are proposed verification steps, not reported test results. Close the window when finished; the sample then terminates.
What changes for a borderless window?
A visible borderless window may still be unable to become key. Apple documents that an ordinary NSWindow without the titled style does not become key by default. The supported customization is to override canBecomeKey in an NSWindow subclass and return true when that window is intended to receive keyboard input. Apple: canBecomeKey
Start with the titled example to establish working text entry. If the interface later requires a borderless window, change the window implementation while retaining the same attachment, editability, and responder checks. Changing the window’s appearance does not replace those requirements.
How should the same editor work inside SwiftUI?
Use NSViewRepresentable to integrate an AppKit text view into a SwiftUI hierarchy. Apple assigns creation and initial configuration to makeNSView(context:), ongoing state updates to updateNSView(_:context:), and coordination with delegate or target-action messages to a coordinator. Apple: NSViewRepresentable
For this editor, a practical design is to return the scroll view as the represented view and keep a reference to its document text view in the coordinator or a custom container. Configure editability during creation. Forward user text changes through the delegate when the surrounding SwiftUI interface needs a binding.
Treat attachment as a separate event from creation. Before requesting focus, check that the editor’s window is non-nil. An AppKit subclass can use viewDidMoveToWindow() to observe hierarchy changes; Apple documents that a nil window there means the view currently resides in no window. This provides a documented lifecycle hook for implementing an attachment-aware focus request. Apple: viewDidMoveToWindow()
As an implementation policy, request initial focus once for the intended presentation, or in response to an explicit action such as Focus Editor. Avoid unconditionally calling makeFirstResponder(_:) on every SwiftUI update: that policy would repeatedly move focus back after the person selects another control. Check the containing window at the time of the request rather than assuming a previously stored window remains appropriate.
Also avoid resetting the editor’s string on every update. In a binding-based wrapper, compare incoming state with the current text and account for marked text and selection before replacing content. The standalone example initializes its string once; a synchronized editor needs an explicit update policy.
Let SwiftUI size the represented root view. Apple warns that directly changing that view’s frame or bounds conflicts with SwiftUI’s layout management. The direct frame setup in the standalone AppKit program is therefore not a sizing pattern to copy onto a SwiftUI-managed root view. Apple: NSViewRepresentable
Does accepting typing require a custom TextKit stack?
For this task, use NSTextView’s existing editing behavior. Apple describes it as the text-system front end that draws text and handles selection, modification, input management, and key bindings. Ordinary typing does not require rebuilding those facilities. Apple: NSTextView
TextKit engine selection is a separate decision from keyboard focus. Apple documents that init(frame:) uses NSLayoutManager by default, while a manually assembled text view can use NSTextLayoutManager on macOS 12 or later through the designated initializer. Choose an engine based on your layout requirements; switching engines is not a substitute for making the editor first responder. Apple: init(frame:textContainer:)
One diagnostic deserves care: reading layoutManager is not necessarily a passive inspection. Apple documents that accessing it on a text view or text container using the newer system can trigger compatibility mode using NSLayoutManager. The example avoids querying it and makes no assumption about which engine the scrollable convenience factory chooses. Apple: NSTextView
A working result is concrete: the intended app window is active and key, its editable text view is the first responder, and typing changes the displayed text. Establish that behavior with the small window first, then preserve the same checks as you add SwiftUI integration, custom layout, or additional controls.
Sources
- NSTextView — Apple.
- isEditable — Apple.
- keyWindow — Apple.
- makeFirstResponder(_:) — Apple.
- initialFirstResponder — Apple.
- makeKeyAndOrderFront(_:) — Apple.
- canBecomeKey — Apple.
- scrollableTextView() — Apple.
- activate() — Apple.
- MainActor — Apple.
- run() — Apple.
- NSViewRepresentable — Apple.
- viewDidMoveToWindow() — Apple.
- init(frame:textContainer:) — Apple.
- Attributes — The Swift Project.