プログラムで作成した NSTextView で入力できるようにするには、編集を有効にし、ウィンドウにアタッチして、そのウィンドウの makeFirstResponder(_:) を呼び出します。ウィンドウはキーウィンドウになれる必要もあります。以下に、完全な例と要点を絞ったトラブルシューティングの手順を示します。
入力できるようにするための前提条件
このガイドでは、複数行のテキストを入力・編集できる小さな Mac アプリのウィンドウを作成する手順を説明します。AppKit を直接使い、エディタを SwiftUI に組み込む場合に対応するライフサイクル上の選択肢も解説します。
満たすべき条件は3つあります。テキストビューで編集が許可されていること、ウィンドウがキーボードイベントを受け取ること、そしてそのウィンドウ内でテキストビューが意図した first responder であることです。Apple の説明によると、アプリケーションの keyWindow は現在キーボードイベントを受け取っているウィンドウです。ウィンドウを表示するだけでは、どのビューでテキストを編集するかは決まりません。Apple: keyWindow
isEditable = true を設定すると、編集が許可され、テキストビューも選択可能になります。Apple のドキュメントではテキストビューはデフォルトで編集可能とされていますが、明示的に設定すると、この例の構成が明確になります。編集可能にするだけでは、キーボード入力先は選択されません。Apple: isEditable
フォーカスを設定するには、親ウィンドウの makeFirstResponder(textView) を使います。Apple は、テキストビューの becomeFirstResponder() を直接呼び出すのではなく、このメソッドを使うよう開発者に明示しています。これにより、ウィンドウが responder 間の切り替えを調整できます。Apple: NSTextView
Swift で編集可能なウィンドウを作る
次のスタンドアロンプログラムは macOS 14 以降を対象とし、macOS SDK を含む現行の Swift ツールチェーンを使います。ウィンドウのタイトルは Local Editor、初期テキストは Sample note です。テキストはメモリ内に保持され、この例では保存処理を実装していません。
完全なプログラムを EditorDemo.swift として保存してください。既存のアプリに2つ目のエントリーポイントとして追加せず、スタンドアロンの実行ファイルとして使ってください。Swift ではプログラムのエントリーポイントは1つだけです。以下の @main 型がその役割を担います。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()
}
}
}
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()
コードはフォーカスを要求する前に、そのコンテナをアタッチします。また、ウィンドウが最初に表示されたときに選択されるビューとして Apple が定義している initialFirstResponder も設定します。その後の makeFirstResponder(_:) 呼び出しでは、エディタを明示的に指定し、結果を確認できるようにしています。初期フォーカスと後からのフォーカス要求は、ウィンドウのライフサイクルにおける異なるタイミングで使われます。Apple: initialFirstResponder、Apple: makeFirstResponder(_:)
ウィンドウはキーウィンドウになれるよう、タイトル付きスタイルを使います。makeKeyAndOrderFront(nil) は、ウィンドウの表示とキーウィンドウ化の両方を要求します。Apple はこれを、キーウィンドウを変えずにウィンドウの表示順序を変更する orderFront(_:) と区別しています。Apple: canBecomeKey、Apple: makeKeyAndOrderFront(_:)
activate() の呼び出しがあるため、このプログラムには macOS 14 以降が必要です。Apple はアクティベーションを保証ではなく要求として説明しています。ウィンドウが表示されても入力を受け付けない場合は、そのウィンドウをクリックしてアプリをアクティベートし、もう一度確認してください。すぐに出力される診断メッセージを、アクティベーションが完了した証拠と解釈しないでください。Apple: activate()
エントリーポイントとデリゲートには @MainActor が付いています。Apple によると、そのエグゼキューターはメインディスパッチキューと同等です。また、セットアップの完了後に AppKit がユーザーイベントをディスパッチできるよう、プログラムは run() で AppKit のイベントループを開始します。Apple: MainActor、Apple: run()
表示されているのに入力を受け付けないエディタを診断する
次の順番で確認してください。アタッチ、ウィンドウの適格性、responder の選択、編集可能かどうかを切り分けられるため、それぞれの結果から次に行うべきことが分かります。
| 確認項目 | 結果の意味 | 次の対応 |
|---|---|---|
textView.window === window |
エディタが意図したウィンドウに属しています。 | false の場合は、フォーカスを要求する前にビュー階層のアタッチを完了します。 |
window.canBecomeKey |
ウィンドウはキーウィンドウになれる状態です。 | false の場合は、ウィンドウのスタイルやカスタムサブクラスを確認します。 |
window.isKeyWindow |
このウィンドウがアプリ内で現在キーウィンドウです。 | アプリをアクティベートし、目的のウィンドウを選択してから再確認します。 |
textView.acceptsFirstResponder |
エディタはフォーカス要求を受け付けます。 | false の場合は、設定とサブクラスのオーバーライドを確認します。 |
window.firstResponder === textView |
このエディタ自体が first-responder 状態です。 | false の場合は、フォーカスの切り替えと現在の responder を確認します。 |
textView.isEditable |
ユーザーによる編集が有効です。 | 編集させる場合は true に設定します。 |
responder の確認は特に役立ちます。makeFirstResponder(_:) には、注意すべき仕様があります。要求された responder が first-responder 状態を拒否し、ウィンドウが代わりに first responder になると、このメソッドは true を返すことがあります。また Apple は、明示的に要求する前に acceptsFirstResponder を確認するよう述べています。そのため、戻り値と、結果として firstResponder が実際にどのオブジェクトを指しているかの両方を確認してください。Apple: makeFirstResponder(_:)
false の結果は、現在の first responder が resign を拒否したことを示している場合があります。その responder の動作と、関係する編集デリゲートを確認してください。たとえば Apple の説明では、テキストビューのデリゲートは textShouldEndEditing(_:) を通じて編集の終了を拒否できます。拒否の原因を解決せずに同じフォーカス要求を繰り返しても、問題は解消しません。Apple: makeFirstResponder(_:)、Apple: NSTextView
基本的な手動確認では、Blue square と入力して Return を押し、Green circle と入力します。その後、矢印キーと Delete キーを使ってテキストを編集します。ウィンドウのサイズを変え、そのまま入力を続けてください。これらは推奨する確認手順であり、実施済みのテスト結果ではありません。完了したらウィンドウを閉じてください。サンプルはその時点で終了します。
枠なしウィンドウでは何が変わるか
表示されていても、枠なしウィンドウはキーウィンドウになれない場合があります。Apple のドキュメントでは、タイトル付きスタイルを持たない通常の NSWindow は、デフォルトではキーウィンドウにならないとされています。サポートされているカスタマイズ方法は、NSWindow のサブクラスで canBecomeKey をオーバーライドし、そのウィンドウがキーボード入力を受け取る必要がある場合に true を返すことです。Apple: canBecomeKey
まずタイトル付きの例でテキスト入力が機能することを確認してください。後で枠なしウィンドウが必要になったら、アタッチ、編集可能かどうか、responder の確認は維持したまま、ウィンドウの実装を変更します。ウィンドウの見た目を変えても、これらの要件は変わりません。
同じエディタを SwiftUI 内で動作させるには
AppKit のテキストビューを SwiftUI のビュー階層に統合するには NSViewRepresentable を使います。Apple の説明では、作成と初期設定は makeNSView(context:) が担当し、状態の更新は updateNSView(_:context:) が担当します。デリゲートや target-action メッセージとの連携にはコーディネーターを使います。Apple: NSViewRepresentable
このエディタでは、スクロールビューをラップされたビューとして返し、コーディネーターまたはカスタムコンテナにそのドキュメント用テキストビューへの参照を保持する設計が実用的です。作成時に編集可能に設定します。周囲の SwiftUI インターフェイスでテキストのバインディングが必要な場合は、デリゲートを通じてユーザーのテキスト変更を渡します。
アタッチは作成とは別のイベントとして扱います。フォーカスを要求する前に、エディタの window が nil でないことを確認してください。AppKit のサブクラスでは viewDidMoveToWindow() を使ってビュー階層の変更を監視できます。Apple の説明では、そこで window が nil なら、そのビューは現在どのウィンドウにも属していません。これにより、アタッチを考慮したフォーカス要求を実装するための、ドキュメント化されたライフサイクルフックを利用できます。Apple: viewDidMoveToWindow()
実装方針として、最初のフォーカスは意図したプレゼンテーション時に一度だけ要求するか、Focus Editor のような明示的な操作に応じて要求してください。SwiftUI の更新ごとに無条件で makeFirstResponder(_:) を呼び出すと、ユーザーが別のコントロールを選んだ後もフォーカスを繰り返し戻すことになります。以前保存したウィンドウが引き続き適切だと思い込まず、要求時点で親ウィンドウを確認してください。
また、更新のたびにエディタの文字列をリセットしないでください。バインディングを使うラッパーでは、入力される状態と現在のテキストを比較し、コンテンツを置き換える前に marked text と選択範囲を考慮します。スタンドアロンの例では文字列を一度だけ初期化します。状態を同期するエディタには、明示的な更新方針が必要です。
ラップされたルートビューのサイズは SwiftUI に管理させてください。そのビューの frame や bounds を直接変更すると、SwiftUI のレイアウト管理と競合すると Apple は警告しています。そのため、スタンドアロンの AppKit プログラムにある直接的なフレーム設定を、SwiftUI が管理するルートビューのサイズ指定に流用しないでください。Apple: NSViewRepresentable
入力を受け付けるために独自の TextKit スタックは必要か
この用途では NSTextView に備わっている編集機能を使います。Apple はこれを、テキストを描画し、選択、変更、入力管理、キー割り当てを処理するテキストシステムのフロントエンドとして説明しています。通常の入力のために、これらの機能を作り直す必要はありません。Apple: NSTextView
TextKit エンジンの選択とキーボードフォーカスは別の問題です。Apple のドキュメントによると、init(frame:) はデフォルトで NSLayoutManager を使います。一方、手動で組み立てたテキストビューは、指定イニシャライザーを通じて macOS 12 以降で NSTextLayoutManager を使えます。レイアウト要件に基づいてエンジンを選んでください。エンジンを切り替えても、エディタを first responder にする必要性は変わりません。Apple: init(frame:textContainer:)
診断の際に注意したい点があります。layoutManager の読み取りは、必ずしも副作用のない確認ではありません。Apple のドキュメントでは、新しいシステムを使うテキストビューまたはテキストコンテナでこれにアクセスすると、NSLayoutManager を使った互換モードが起動する場合があるとされています。この例ではこれを問い合わせず、スクロールビュー用の便利なファクトリーメソッドがどのエンジンを選ぶかについても仮定していません。Apple: NSTextView
動作している状態は明確です。意図したアプリのウィンドウがアクティブかつキーウィンドウで、編集可能なテキストビューが first responder となり、入力した文字が表示テキストに反映されます。まず小さなウィンドウでこの動作を確認し、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。
- Attributes — The Swift Project。