若要讓以程式建立的 NSTextView 接受輸入,請啟用編輯功能、將它附加至視窗,並呼叫該視窗的 makeFirstResponder(_:)。視窗也必須能成為鍵盤焦點視窗。以下提供完整範例和精簡的疑難排解步驟。
輸入文字前必須滿足哪些條件?
本指南說明一項工作:建立一個小型 Mac App 視窗,讓使用者能輸入及編輯多行文字。指南直接使用 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。請將它作為獨立可執行檔使用,不要把它新增至已有另一個進入點的 App。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 或 Command Line Tools 的 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 說明,啟用 App 是一項要求,而非保證。如果視窗出現後仍無法輸入文字,請點選視窗來啟用 App,然後再檢查一次。不要將立即印出的診斷訊息視為啟用已完成的保證。Apple:activate()
進入點和委派型別都標記為 @MainActor;Apple 將其執行器定義為等同於主派送佇列。程式也透過 run() 啟動 AppKit 的事件迴圈,讓 AppKit 在設定完成後派送使用者事件。Apple:MainActor、Apple:run()
排查可見但仍無法輸入文字的編輯器
請依序完成下列步驟。這些步驟將檢查檢視是否已附加、視窗是否符合條件、回應者是否已選取,以及是否允許編輯;每項觀察結果都會指出明確的下一步行動。
| 檢查項目 | 結果代表的意義 | 下一步行動 |
|---|---|---|
textView.window === window |
編輯器屬於預期的視窗。 | 若為 false,請先完成檢視階層的附加,再要求焦點。 |
window.canBecomeKey |
視窗符合接收鍵盤焦點狀態的條件。 | 若為 false,請檢查視窗樣式或自訂子類別。 |
window.isKeyWindow |
這個視窗目前是 App 中的鍵盤焦點視窗。 | 啟用 App 並選取預期的視窗,然後再次檢查。 |
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 訊息的協調則交由 coordinator 負責。Apple:NSViewRepresentable
對這個編輯器而言,實用的設計是將捲動檢視作為代表檢視回傳,並在 coordinator 或自訂容器中保留其文件文字檢視的參照。在建立時設定可編輯性。若周圍的 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
成功的結果很明確:預期的 App 視窗處於啟用狀態且是鍵盤焦點視窗;其中可編輯的文字檢視是第一回應者;輸入文字會改變畫面上顯示的內容。先用小型視窗確認這項行為,再於加入 SwiftUI 整合、自訂版面配置或其他控制項時,維持相同的檢查方式。
資料來源
- 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。
- 屬性 — Swift 專案。