ウィンドウ関係(macOS)

  1. マルチディスプレイ時の問題点
  2. NSWindow
    1. ウィンドウの非表示
    2. ウィンドウの表示
    3. 自分を閉じる
    4. おまけ
  3. スクロールビュー
  4. ウィンドウのアクティブ化
    1. AppKit (NSWindow) の場合
    2. SwiftUIの場合
  5. CGRect の型について
    1. absの意味
    2. minの意味
  6. 🔹 ((CGRect) -> Void)
  7. SwiftUIのViewで矩形描写
  8. エントリに複数View用意して呼ぶ
  9. SCShareableContent (キャプチャするウィンドウフィルタ)
  10. アクティブウィンドウの取得のあれこれ
    1. 最前面のウィンドウの SCDisplay の取得
    2. アクティブウィンドウがある SCDisplay の取得
    3. マウスカーソルがある SCDisplay の取得
    4. 指定したディスプレイの SCDisplay を取得する
    5. 接続ディスプレイの取得とそのログ吐き出し
  11. 毎回ウィンドウの位置を MainView の座標にリセットする
  12. ウィンドウの完全終了
  13. ウィンドウのリサイズ
  14. キャプチャウィンドウ(失敗)
  15. プロパティ情報
    1. NSWindow
    2. SCDisplay
    3. CGDirectDisplayID
    4. NSScreen
  16. 🟦 .onDrop の構造を理解しよう
    1. URLでファイルを判定する
    2. NSItemProvider について
  17. 翻訳機能が来た時のメモ
  18. NSViewRepresentable
    1. makeNSView(context:)
    2. updateNSView(_:context:):
    3. makeCoordinator() (任意):
    4. dismantleNSView(_:coordinator:)
    5. 全体の流れ
    6. なぜCoordinatorが必要?
    7. 別の概要説明
  19. ウィンドウイベントの取得
    1. NSEvent.addLocalMonitorForEvents
  20. この点はリサイズハンドルか判定
  21. NSView でキーを拾う例
  22. NSTextView 作成例
  23. Main Thread only メモ

マルチディスプレイ時の問題点

① マルチディスプレイの座標体系は “左上原点ではない”

macOS はこういう座標体系になっている:

例:

→ ディスプレイの frame は絶対座標(グローバル座標)なので、
macOS が再配置すると値が変動し得る。

② “NSScreen.frame” と “SCDisplay.frame” は100%一致しない

Apple内部でも:

NSScreen = Cocoa系座標
SCDisplay = ScreenCaptureKitの独自座標

となっていて、

  • UIスケール
  • Retina / 非Retina
  • Rotation
  • メニューバーの高さ

などで微妙にズレる。

実際、Appleのフォーラムでも同じ例が大量に報告されている。

③ macOS 15 ではディスプレイの位置変更が “遅延適用” されることがある

Mission Control で画面構成が変わったあとなど:

  • NSScreen がすぐ更新される
  • ScreenCaptureKit が数百ms遅れて更新される

という差分が発生する。

→ 座標比較だとズレることがある。

NSWindow

最小

矩形描写するView

画面全体を覆う透明ウィンドウ。

ウィンドウの非表示

ウィンドウの表示

自分を閉じる

おまけ

WindowLevel を .statusBar や .floating にする

.screenSaver は強力だが OS の hide/unhide と衝突しがちなので、
以下のレベルでも十分最前面になる:

おすすめは .statusBar
常に最前面で、NSApp.hide()にも巻き込まれにくい。

スクロールビュー

方法1: SwiftUIのScrollViewを使用(シンプル)

方法2: NSScrollViewを使用(より細かい制御)

NSScrollViewでラップしたNSViewRepresentableを作成:

方法3: CanvasNSView自体にスクロール機能を組み込む

既存のCanvasNSViewを修正する場合:

おすすめの実装

方法2のNSScrollViewを使う方法がおすすめです。理由:

  • ✅ スクロールバーの表示/非表示を細かく制御できる
  • ✅ ズーム機能も追加しやすい
  • ✅ macOSネイティブのスクロール動作
  • ✅ 大きな画像でもパフォーマンスが良い

ズーム機能の追加(オプション)

スクロールビューにズーム機能も追加できます:

ウィンドウのアクティブ化

AppKit (NSWindow) の場合

NSApplicationのインスタンスと、アクティブにしたいウィンドウのNSWindowインスタンスが必要です。

SwiftUIの場合

SwiftUIでは、直接的なウィンドウのアクティベーションメソッドは提供されていませんが、NSApplication.shared.activate(ignoringOtherApps: true)を呼び出してアプリケーションをアクティブ状態にできます。
特定のウィンドウを表示または最前面に移動したい場合は、@Environment(.openWindow)アクションを使用し、SwiftUIに管理されているウィンドウを識別子(Hashableな値)で指定する方法があります。すでに開いているウィンドウであれば、それが再利用されて前面に表示されます。

CGRect の型について

→ 四角形を表す構造体。位置(x,y)+大きさ(width,height)。
Swift の標準型で、UIKit/CoreGraphics でよく使う。

absの意味

absはCGRectのプロパティではなく、Swiftの標準ライブラリに含まれる絶対値を計算する関数です。 

CGRectの文脈では、矩形の幅や高さが負の値になった場合に、その絶対値(正の値)を取得したい場合などに利用されます。例えば、幅が-100であればabs(-100)100を返します。 

minの意味

minは、CGRectのプロパティ名の一部(例: minX, minY)として使用されます。これらは、矩形の各座標軸における最小値を返します。 

  • minX: 矩形のX座標の最小値。
  • minY: 矩形のY座標の最小値。 

これらの値は、矩形の位置とサイズに基づいて自動的に計算される「get-only」プロパティ(読み取り専用)です。 

var onComplete: ((CGRect) -> Void)? は何を意味する?

これは 「CGRect を引数に取り、返り値が Void(=なし)のクロージャを格納できる変数」 という意味。

分解すると:

🔹 ((CGRect) -> Void)

これは 関数の型

  • 引数:CGRect
  • 戻り値:Void(=戻り値なし)

つまりこんなものを入れられる:

簡単に言うと onComplete(結果内容) で呼び出し元の rect に 結果内容 が入る。
処理が終わって rect に反映させたくなったら使うと便利。

SwiftUIのViewで矩形描写

🔍 動作説明

✔ .onChanged

  • 最初のドラッグ開始時に startPoint を記録
  • ドラッグ中は currentPoint を更新
  • この2点から矩形が描画される

✔ .onEnded

  • 最終位置を使って CGRect を確定
  • OCR する範囲として渡すのに最適
  • あとは onComplete(rect) のようにコールバックに渡せばOK

エントリに複数View用意して呼ぶ

SCShareableContent (キャプチャするウィンドウフィルタ)

🔍 excludingDesktopWindows(false, onScreenWindowsOnly: true) の意味

Apple公式ドキュメントに基づいて分解すると:

🟦 第1引数:excludeDesktopWindows

● false → デスクトップウィンドウも含める

つまり:

  • 壁紙
  • アイコン
  • Finder のデスクトップレイヤ
    …などもキャプチャ対象として扱う。

● true → デスクトップ(壁紙など)は含まない

ゲームキャプチャなどで有効。

🟩 第2引数:onScreenWindowsOnly

● true → 現在画面に表示されているウィンドウだけ

隠れているウィンドウ(最小化・背面)は対象外。

例:

  • 違うデスクトップにあるウィンドウ
  • minimize されたウィンドウ
  • 画面外にオフセットされたウィンドウ
    などは除外。

● false → 表示されていないウィンドウも含める

アプリを丸ごとキャプチャしたい時に使う。

excludeDesktopWindowsonScreenWindowsOnly用途
falsetrue選択範囲SSR(あなたのアプリ)/一般的なスクショ
truetrueゲーム配信(壁紙をキャプチャしたくない)
falsefalse全ウィンドウ一括キャプチャ/画面外のアプリ画面取り込み
truefalse特殊用途(めったに使わない)

📌 注意点

ScreenCaptureKit のフィルタは
キャプチャ結果に直結するのでめちゃくちゃ重要

  • excludeDesktopWindows = true だと
     → 壁紙やアイコンがキャプチャされず背景が真っ黒になる
  • onScreenWindowsOnly = false だと
     → 他のDesktop Space のウィンドウまで全部混ざる
    などの副作用がある。

アクティブウィンドウの取得のあれこれ

最前面のウィンドウの SCDisplay の取得

macOS では「最前面のウィンドウ」を
CGWindowListCopyWindowInfo が返してくれる。

使い方

アクティブウィンドウの CGRect(画面上の位置)を取得
kCGWindowBounds の中に入ってる:

そのウィンドウが乗っているディスプレイを取得
macOS はウィンドウ → ディスプレイの判定が簡単。

使い方

ScreenCaptureKit の「表示対象ディスプレイ」に設定する

現在のアクティブウィンドウのある画面を
SCShareableContent の display として使いたい時

これは ディスプレイを正しく特定して、その screen に対応する SCDisplay を検索する という流れになる。

📌 さらに便利なユーティリティ関数を作るとこうなる

上記をまとめたコード

アクティブウィンドウがある SCDisplay の取得

前提、プロセスIDの取得

🧩 ステップ①

プロセスIDから「そのアプリの一番上のウィンドウ」を CGWindowList で取得

🧩 ステップ②

そのウィンドウの位置から、属するディスプレイ(NSScreen)を取得

🧩 ステップ③

NSScreen と SCDisplay をマッチングする
ScreenCaptureKit は frame を使ってディスプレイを判定できる。

🧩 ステップ④

ぜんぶ合わせた「PID → SCDisplay」関数

マウスカーソルがある SCDisplay の取得

指定したディスプレイの SCDisplay を取得する

メインディスプレイを取得

セカンダリディスプレイを取得(メイン以外の最初のディスプレイ)

DisplayIDを指定してディスプレイを取得

インデックスでディスプレイを取得(0がメインとは限らない)

SCDisplay、NSScreenをディスプレイIDで一致させる

🟦 注意点
❗ NSScreen の displayID は private API
(value(forKey:) を使う時点で非公開の可能性)

接続ディスプレイの取得とそのログ吐き出し

毎回ウィンドウの位置を MainView の座標にリセットする

「SelectionOverlay ウィンドウを MainView と同じ位置・同じ画面に出したいが、
2回目以降は前回の位置のまま残り続ける

これは SwiftUI の Window Scene は“同じ id のウィンドウを再利用する” という仕様のため。

つまり:

なら、最初に作られたウィンドウインスタンスが
ずっと生き続ける(位置やサイズも保持する)

だから、

  • 1回目 → MainView の画面に正しく出る
  • 2回目 → 前回閉じた場所にそのまま再生成される(正しくは「再表示」)

となっている。

「毎回決まった場所に出す」には、
WindowScene の NSWindow を捕まえて位置を指定する

SwiftUI の .windowStyle や .windowLevel では位置制御ができないので
AppKit でやる必要がある。

🔧 手順

① SelectionOverlay の Scene に .windowPosition(.center) を追加

(※ただしこれはセンター基準で、MainView の画面ではない)

ただしこれだけだと「MainView のあるディスプレイ」にはならない。

② MainView のある画面位置に手動で配置

SelectionOverlayView の onAppear に次を追加:

※ MainView のタイトルは必要なら設定:

これで 毎回 MainView のある画面に出る

ウィンドウの完全終了

Debug ビルドの .app を Dock から再表示したあと
ボタンを押すとウィンドウが増えてしまう

これは SwiftUI の WindowScene の仕様
閉じたウィンドウとプログラムで生成したウィンドウが“別物扱いになる”問題が原因。

結論から言うと——

🚨 結論

ウィンドウを「赤い×ボタンで閉じた時」に、SwiftUI の WindowScene は “閉じられたウィンドウを記憶” してしまう。

そして

  • Dock からアプリを再度アクティブにすると
     → MainWindowScene が自動再生成される
  • あなたが openWindow(id:) を呼ぶと
     → SelectionOverlayScene が新規生成される

結果として ウィンドウが増える

💡 解決策(ベスト3)

✅ 方法1(ベスト)

赤×ボタンで閉じた時に「完全終了」させる

これは最もシンプルで正しい対処。
macOS の常駐アプリでないなら正解。

または MainView の window delegate に:

✅ 方法2

ウィンドウを × で閉じさせない(常駐アプリ方式)

× を押しても閉じず「非表示」にする。

または window.delegate で:

✅ 方法3

MainWindowScene を自動再生成させない

SwiftUI は Dock をクリックするたびに WindowScene を再作成する。
これを止めるには:

Main ウィンドウを閉じた時に “永久に破棄” する

または

しかし、これは設定が少し複雑なので、方法1 or 方法2 が最適。

ウィンドウのリサイズ

現在フォーカス中のウィンドウをリサイズする

特定のウィンドウだけリサイズ

ウィンドウを常に前面にする

キャプチャウィンドウ(失敗)

プロパティ情報

NSWindow

ウィンドウの状態:

  • window.title – ウィンドウのタイトル
  • window.isVisible – ウィンドウが表示されているか
  • window.isKeyWindow – キーウィンドウ(入力フォーカスがある)か
  • window.isMainWindow – メインウィンドウか
  • window.isMiniaturized – 最小化されているか
  • window.isZoomed – ズーム(最大化)されているか
  • window.level – ウィンドウレベル(前面表示の優先度)
  • window.alphaValue – 透明度(0.0〜1.0)
  • window.isOpaque – 不透明かどうか

ウィンドウの属性:

  • window.styleMask – ウィンドウスタイル(タイトルバー、リサイズ可能など)
  • window.backgroundColor – 背景色
  • window.hasShadow – 影があるか
  • window.isMovable – 移動可能か
  • window.isMovableByWindowBackground – 背景ドラッグで移動可能か

コンテンツ関連:

  • window.contentView – コンテンツビュー
  • window.contentViewController – コンテンツビューコントローラ
  • window.toolbar – ツールバー

ディスプレイ関連:

  • window.screen – ウィンドウが表示されているNSScreen
  • window.backingScaleFactor – Retinaスケール係数(1.0または2.0など)

その他:

  • window.windowNumber – ウィンドウ番号(システム内の一意識別子)
  • window.orderingMode – ウィンドウの並び順モード
  • window.collectionBehavior – Spacesやフルスクリーンの動作

SCDisplay

ディスプレイの基本情報:

  • activeDisplay.displayID – ディスプレイの一意識別子(CGDirectDisplayID
  • activeDisplay.width – ディスプレイの幅(ピクセル)
  • activeDisplay.height – ディスプレイの高さ(ピクセル)

その他の情報:

  • activeDisplay.frame – ディスプレイのフレーム(CGRect)※座標も含むため参考程度

CGDirectDisplayID

ディスプレイの基本情報:

  • CGDisplayBounds(displayID) – ディスプレイの位置とサイズ(CGRect
  • CGDisplayPixelsWide(displayID) – 幅(ピクセル)
  • CGDisplayPixelsHigh(displayID) – 高さ(ピクセル)
  • CGDisplayScreenSize(displayID) – 物理サイズ(ミリメートル)

ディスプレイの状態:

  • CGDisplayIsActive(displayID) – アクティブかどうか
  • CGDisplayIsAsleep(displayID) – スリープ中かどうか
  • CGDisplayIsBuiltin(displayID) – 内蔵ディスプレイかどうか
  • CGDisplayIsMain(displayID) – メインディスプレイかどうか
  • CGDisplayIsOnline(displayID) – オンラインかどうか
  • CGDisplayIsStereo(displayID) – ステレオディスプレイかどうか

色・モード情報:

  • CGDisplayCopyColorSpace(displayID) – カラースペース
  • CGDisplayModeGetWidth(mode) / CGDisplayModeGetHeight(mode) – 現在の解像度
  • CGDisplayModeGetRefreshRate(mode) – リフレッシュレート

回転情報:

  • CGDisplayRotation(displayID) – 回転角度(0, 90, 180, 270度)

ミラーリング情報:

  • CGDisplayIsInMirrorSet(displayID) – ミラーリングセットに含まれているか
  • CGDisplayMirrorsDisplay(displayID) – ミラーリングしているディスプレイID

NSScreen

基本情報:

  • screen.localizedName – ディスプレイ名(例: “Built-in Retina Display”)
  • screen.depth – 色深度
  • screen.deviceDescription – デバイス情報の辞書

座標・サイズ情報:

  • screen.frame – スクリーン全体のフレーム(CGRect
  • screen.visibleFrame – メニューバーやDockを除いた表示可能領域
  • screen.backingScaleFactor – Retinaスケール係数(1.0, 2.0など)

色関連:

  • screen.colorSpace – カラースペース(NSColorSpace?
  • screen.maximumExtendedDynamicRangeColorComponentValue – HDRの最大輝度値
  • screen.maximumPotentialExtendedDynamicRangeColorComponentValue – HDRの潜在的最大輝度値
  • screen.maximumReferenceExtendedDynamicRangeColorComponentValue – HDR参照最大値

リフレッシュレート(macOS 12+):

  • screen.maximumFramesPerSecond – 最大フレームレート(Hz)
  • screen.minimumRefreshInterval – 最小リフレッシュ間隔
  • screen.maximumRefreshInterval – 最大リフレッシュ間隔
  • screen.displayUpdateGranularity – 更新の粒度

ディスプレイ設定:

  • screen.auxillaryTopLeftArea – 左上の補助領域
  • screen.auxillaryTopRightArea – 右上の補助領域

静的プロパティ:

  • NSScreen.main – メインスクリーン(メニューバーがある画面)
  • NSScreen.screens – 接続されている全スクリーンの配列
  • NSScreen.deepest – 最も色深度が高いスクリーン

🟦 .onDrop の構造を理解しよう

SwittUlの
.onDrop はこういう形:

✔ of: [.fileURL]
受け入れるデータの種類を指定する配列。
・fileURL→「ファイルのURLをドロップできます」
・PDF だけ許可したいなら.pdfも使える
例:

✔ isTargeted: nil

ドラッグ中にホバーしているかどうかを状態で受け取りたいときに使います。
本来は「ドラッグが乗ってる間 true にするための State」。
視覚的にハイライトしたいときに使う。

例:

isHovering が true/false になるので背景色を変える…などが可能。

✔ perform: handleDrop(providers:) の正体

([NSItemProvider]) -> Bool という関数を渡す必要がある。

ドラッグ&ドロップされたアイテム(ファイルなど)が
NSItemProvider の配列として渡される

よく使うテンプレ

✔ .onDrop(of: [...]) に渡す三要素まとめ

引数意味
of:ドロップ可能なデータタイプ[.fileURL][.pdf]
isTargeted:ドラッグが乗ってるかどうかnil$isHovering
perform:実処理(ファイル取り込み)handleDrop

勘違いしてた事:

Viewに .onDrop はついているが SwiftUI の View にファイルを落としても意味ない。
あくまで SwiftUI の View 外のウィンドウにファイルを落とさないと .onDrop は反応しない。(テキストエディタにファイルを落としてたlol)

URLでファイルを判定する

.pathExtension.lowercased()

より堅牢な方法: PDFDocumentの生成でチェック(PDFのみ)

NSItemProvider について

PDF処理の実行

なぜこんな複雑な変換が必要?

  • NSItemProviderの仕様: データをAny?型で返すため
  • ファイルパスの形式Data → 文字列 → URLの順で変換する必要がある
  • 安全性: 各ステップで失敗する可能性があるため、guard letで確認

翻訳機能が来た時のメモ

NSViewRepresentable

NSViewRepresentable は、SwiftUIアプリケーション内でAppKitフレームワークのNSViewを利用できるようにするプロトコルです。macOS向けのSwiftUI開発において重要な役割を果たします。

これは、iOSにおけるUIViewRepresentableのmacOS版と考えてください。

主な目的と機能

  • AppKitビューの統合: SwiftUIにはまだ組み込まれていない、またはSwiftUIで再現するのが難しい高度な機能を持つ既存のAppKitビュー(例:カスタム描画ビュー、特定のテキストエディタ、地図ビューなど)を、そのままSwiftUIビュー階層内に配置できるようになります [1, 3]。
  • 相互運用性: SwiftUIの新しい宣言的なパラダイムと、AppKitの古い命令的なパラダイムの橋渡しをします [1]。

実装に必要な主なメソッド

NSViewRepresentableプロトコルに準拠するには、主に以下のメソッドを実装する必要があります。

makeNSView(context:)

AppKitのビュー(NSViewのサブクラス)のインスタンスを作成し、初期設定を行うためのメソッドです。SwiftUIビューが表示される準備ができたときに一度だけ呼び出されます。

最初に1回だけ呼ばれる
効果:

  • 実際のNSViewを作成してSwiftUIに渡す
  • startMonitoring()でキーイベントの監視を開始
  • このビューがSwiftUIの階層に追加される

updateNSView(_:context:):

SwiftUIの状態(@State@Bindingなどで管理されているデータ)が変更されたときに呼び出されます。このメソッドを使って、基となるAppKitビューのプロパティを最新のSwiftUIの状態に合わせて更新します。

親の状態が変わる度に呼ばれる
効果:

  • 今回は空だが、通常は@Bindingvarが変わった時にNSViewを更新する
  • 例:isRecordingが変わった時に何かしたい場合はここに書く

makeCoordinator() (任意):

  • AppKitビューから発生するイベント(デリゲートメソッドなど)を処理し、SwiftUI側に伝えるための「コーディネーター」オブジェクトを作成します。これにより、両方向のデータフロー(SwiftUIからAppKit、AppKitからSwiftUI)が可能になります。

最初に1回だけ呼ばれる
効果:

  • SwiftUIとAppKit(NSView)の橋渡し役を作成
  • parentで親のプロパティ(@Bindingなど)にアクセスできる
  • 状態を保持し続ける(ビューが再描画されても同じインスタンスが使われる)

要するに、既存のmacOSネイティブ機能や複雑なカスタムビューをSwiftUIプロジェクトに組み込むための標準的な方法がNSViewRepresentableです。

dismantleNSView(_:coordinator:)

ビューが破棄される時に1回呼ばれる。
効果:

  • イベントモニターを解除(超重要!
  • 解除しないとメモリリークが発生する
  • ビューが消えてもモニターが残り続けてしまう

全体の流れ

  1. SwiftUIがNSViewRepresentableを作成
  2. makeCoordinator() → Coordinatorインスタンス作成(1回のみ)
  3. makeNSView() → NSView作成 & イベント監視開始(1回のみ)
  4. updateNSView() → 状態変更時に呼ばれる
  5. dismantleNSView() → ビュー破棄時にイベント監視停止(クリーンアップ)

なぜCoordinatorが必要?

理由:

  • NSEvent.addLocalMonitorForEventsのクロージャ内で@Bindingを更新したい
  • SwiftUIの構造体は値型なので、直接参照できない
  • Coordinatorがクラス(参照型)なので、状態を保持できる

これでself.parent.isRecordingのように親の状態を変更できます!

別の概要説明

(エヌエスビューリプレゼンタブル)とは、SwiftUIでmacOSアプリを開発する際に、従来のAppKitフレームワークのNSView(macOSネイティブのUI部品)をSwiftUIのビューとして利用・統合するための「橋渡し役」となるプロトコルです。SwiftUIとAppKitのハイブリッド開発(共存)を可能にし、既存のNSView資産を活用したり、SwiftUIでは提供されていない高度なmacOS固有のUI機能を使いたい場合に必須となります。 

具体的に何をするものか?

  1. AppKitのNSViewをラップ(包む)NSViewをSwiftUIのViewとして扱えるようにします。
  2. データ連携NSViewとSwiftUIの間でデータの受け渡し(バインディング)を管理します。
  3. ライフサイクル管理NSViewの作成、更新、破棄といったライフサイクルをSwiftUIの仕組みに適合させます。

なぜ使うのか?

  • レガシー資産の活用: 既存のmacOSアプリで使われているNSView(例えばカスタム描画用のNSViewや複雑なテキスト表示コンポーネントなど)を、新しいSwiftUIアプリに組み込みたい場合。
  • macOS固有機能の利用: SwiftUIにはない、macOS特有の高度なUI機能やパフォーマンスが必要な場合に、AppKitのNSViewを直接利用するため。
  • 段階的な移行: SwiftUIへの移行をスムーズにするため、一部の画面やコンポーネントだけをSwiftUIにし、他はAppKitのまま残すハイブリッド構成で利用する。 

使用例(簡単なイメージ)
SwiftUIでMyNSViewWrapper()のようにNSViewRepresentableを実装した構造体(ビュー)を記述すると、その中でmakeNSViewメソッドでNSViewインスタンスを作成し、updateNSViewメソッドでSwiftUIからのデータに基づいてNSViewを更新する、といったコードを書きます。 

まとめると、「SwiftUI」と「macOSのネイティブUI(AppKitのNSView)」を繋ぐための公式なアダプター(変換器)NSViewRepresentableです

ウィンドウイベントの取得

NSEvent.addLocalMonitorForEvents

NSNSEvent.addLocalMonitorForEvents(matching:handler:) は、macOS アプリケーション内で発生する特定のイベント(キーボード入力、マウス操作など)を、本来のイベントハンドラにディスパッチされる前に監視・処理するためのメソッドです。 

使い方と特徴

  • 目的: アプリケーション内のイベントフローを傍受し、イベントをログに記録したり、修正したり、破棄したりすることができます。
  • ローカル監視addGlobalMonitorForEvents とは異なり、このメソッドは自分のアプリケーションに送られるイベントのみを対象とします。
  • 戻り値: ハンドラ(クロージャ)は監視対象の NSEvent オブジェクトを受け取り、NSEvent または nil を返す必要があります。
  • 戻り値(モニターオブジェクト): このメソッドは、後でイベント監視を停止するために使用する不透明なイベントハンドラオブジェクト (Any?) を返します。監視を停止するには、このオブジェクトを NSEvent.removeMonitor() に渡します。 

実装例

NSViewController の viewDidLoad でキーボードイベント (.keyDown) を監視する例です。

注意点

  • モニターの削除: メモリリークや意図しない動作を防ぐため、イベント監視が不要になったら必ず NSEvent.removeMonitor(_:) を呼び出して停止する必要があります。
  • イベントタイプmatching パラメータには、NSEvent.EventTypeMask で定義されている様々なイベントタイプ(例: .mouseMoved.leftMouseDown.flagsChanged など)を指定できます。
  • Responder Chain との違い: 通常のイベント処理は Responder Chain に沿って行われますが、ローカルモニターはその前にイベントを捕捉します。そのため、keyDown(with:) などの標準的なレスポンダメソッドよりも先に処理されます。

この点はリサイズハンドルか判定

NSView でキーを拾う例

NSTextView をサブクラス化

生成時に差し替える

NSTextView 作成例

確定処理(Enter / フォーカスアウト)

CATextLayer を作成

Undo / Redo(テキスト)

Shift 押し判定を取得する?未確認

Main Thread only メモ

項目ルール
NSSavePanelMain Thread only
NSOpenPanelMain Thread only
NSWindow / NSViewMain Thread only
CGContext 描画Background OK

コメント

タイトルとURLをコピーしました