← macOS knowledge

CGEvent による入力イベント合成と、
審査・配布で詰まる理由

調査日: 2026-07-10

この知見の出どころ

本メモは、iPhone から Mac のキーボード/マウスを操作するリモート入力アプリ MonoFlick の開発を通じて判明したことを、Apple 公式ドキュメントとあわせて整理したものです。Mac コンパニオン側で OS に入力を注入する層に CGEvent を使っており、その権限モデルと App Store 審査の噛み合わなさが本メモの主題です。
foo-tec.com/monoflick/

0. 要点(TL;DR)

1. CGEvent とは

CGEvent は Core Graphics の Quartz Event Services が提供する低レベルイベント API。実デバイスを介さずにキーボード/マウスイベントをプログラムから合成し、OS のイベントストリームへ投入できる。ヘッダは <CoreGraphics/CGEventTypes.h> / <CoreGraphics/CGEventSource.h>

イベントソース(CGEventSource)の状態で挙動が変わる:

キーコードは Carbon/HIToolbox/Events.hkVK_ANSI_A 等で、キーボードレイアウト非依存の物理キー位置ベース。任意文字を直接入れたい場合は CGEventKeyboardSetUnicodeString(event, len, &chars)キーコード不問の Unicode 文字注入ができる。日本語のような IME 経由の文字はこのパスを使うと、IME を介さず最終文字列をそのまま送り込める。

2. Accessibility(TCC)という関門

自プロセス内のイベント処理と違い、他アプリを外部から操作する目的で CGEvent を使うには Accessibility 権限が要る。ユーザーが「システム設定 → プライバシーとセキュリティ → アクセシビリティ」で明示的に許可する必要がある。プログラムからは AXIsProcessTrustedWithOptions() で許可状態の確認とプロンプト表示ができる。

最初の罠: silent fail

未許可の状態で CGEventPost を呼んでも、API はエラーを返さず成功したかのように振る舞い、実際には何も起きない。例外もログも出ないため「コードは正しいのに動かない」状態になり、原因が権限だと気づくまで時間を溶かす。実装では起動時・操作前に AXIsProcessTrusted() で明示チェックし、未許可なら動線を出すのが必須。

TCC 権限には開発体験を悪くする固有事情がある。

3. なぜ入力系アプリは審査・配布で詰まるのか

「CGEvent を使うと審査に落ちる」と一括りにされがちだが、原因は段階が違う 3 つに分かれる。順に潰していかないと理由の切り分けができない。

3.1. 根因: Mac App Store のサンドボックスと非互換

Mac App Store で配布するアプリは App Sandbox が必須。サンドボックスは「そのアプリが必要とするリソースだけに権限を絞る」仕組みで、任意の他アプリへグローバルに入力を送り込む動作は、この「絞る」思想の対極にある。Xcode でも Slack でもゲームでも、フォアグラウンドの任意のアプリを外から操作する——という CGEvent 注入の本質は、サンドボックスが許す範囲に収まらない。

3.2. App Store に載せる構成でも詰まる点: permission の説明不足

アプリを分割し、入力注入をしない側(例: iPhone アプリ本体)を App Store に出す構成にしても、権限プロンプトまわりの作法で落ちる。App Store Review Guideline 2.5.1 は「permission を要求する前に、なぜ必要かをユーザーに明示せよ」を求める。

3.3. usage description が抽象的だと "vague reject"

ローカルネットワークや Bluetooth を使うリモート入力アプリでは、NSLocalNetworkUsageDescription / NSBluetoothAlwaysUsageDescription 等の 用途説明文が審査対象になる。「Mac との接続に使用します」程度の抽象的な文言は典型的な reject パターン。

具体化の例(ローカルネットワーク)

✗ 「近くの Mac との接続にローカルネットワークを使用します。」
✓ 「Mac コンパニオンアプリへキーストロークやポインタ操作を送信するため、ローカルネットワーク (Bonjour) を使用します。広告・解析・端末識別には使用しません。」

3.4. secure input(パスワード欄)への配慮

パスワードフィールド等では OS の セキュアインプットが有効になり、注入したイベントは遮断される。macOS では IsSecureEventInputEnabled() で検出できる。ここを無視すると「入力できない不具合」に見えるので、「セキュア入力中のため送信できません」とユーザーに知らせるのが親切であり、審査上も「意図した挙動」を示せる。

4. 用途で最適な方式が違う

一律「キーコードを送る」では破綻する。用途ごとに適した API/モードが異なる。

用途 適した方式(macOS) 理由
日本語など IME 文字 CGEventKeyboardSetUnicodeString IME を介さず最終文字列を直接注入できる。
ショートカット(Cmd+C 等) キーコード指定(kVK_* アプリは「キーの組み合わせ」を見るので Unicode 注入では効かない。
ゲーム(WASD / 視点移動) キーコード + 相対マウス移動 ゲームは物理キー位置・相対 delta を読む。Unicode 注入や絶対座標は通らない。
パスワード欄 (注入不可) セキュアインプットが遮断。IsSecureEventInputEnabled() で検出し UI で告知。

5. 他 OS の対応物と権限モデルの非対称

入力注入 API 自体はどの OS でも枯れているが、権限の取り方が OS ごとに大きく異なる。横断アプリ(リモート入力 / AI の computer use 等)を作るときは、この非対称性を抽象化層で吸収する設計が要る。

OS API 権限モデル 未許可時の挙動
macOS CGEvent Accessibility(TCC)で明示許可。bundle ID + 署名チェーンに紐づく。 silent fail(成功扱いで何も起きない)
Windows SendInput UAC の Integrity Level ベース。承認 UI は無い。 Lower IL → Higher IL は OS が遮断
Linux X11 XTest(XTestFakeKeyEvent X server へ接続できれば可。xhost 設定に従う。 DISPLAY 無しで失敗。Wayland では不可
Linux Wayland /dev/uinput / portal uinput デバイスの RW 権限、または RemoteDesktop portal でユーザー承認。 権限なしなら open 失敗

リモート入力アプリは discovery(mDNS/Bonjour)+ injection(CGEvent 等)の二層構造で成立する。discovery 層は IETF 標準の mDNS/DNS-SD で横串が通るのに対し、injection 層は OS ごとに完全に別 API・別権限モデル。Universal Control / Mouse Without Borders / Synergy 系はすべてこの構造で、後者の非対称性がクロスプラットフォーム化の壁になる。

6. 実装上の含意

まとめ

CGEvent は macOS の標準的な入力注入 API だが、他アプリを操作するには Accessibility 権限が必須で、未許可時は silent fail する。そして入力系アプリが審査で詰まる根因は権限ではなく Mac App Store サンドボックスとの非互換——多くは App Store 外配布が正解になる。App Store に載せる部分でも、permission 前の説明・usage description の具体化・secure input 配慮を外すと "vague reject" を招く。

7. 出典