本メモは、iPhone から Mac のキーボード/マウスを操作するリモート入力アプリ MonoFlick の開発を通じて判明したことを、Apple 公式ドキュメントとあわせて整理したものです。Mac コンパニオン側で OS に入力を注入する層に CGEvent を使っており、その権限モデルと App Store 審査の噛み合わなさが本メモの主題です。
foo-tec.com/monoflick/
0. 要点(TL;DR)
- macOS で OS にキーボード/マウス入力を「合成」して注入する標準 API は
CGEvent(Core Graphics / Quartz Event Services)。他アプリを外から操作するリモート入力・自動化アプリはこれを使う。 - 他プロセスへ入力を送るには Accessibility(TCC)権限が要る。未許可でも
CGEventPostは成功したように返るのに実際は何も起きない(silent fail)——これがデバッグを難しくする最初の罠。 - 審査で詰まる根因は権限そのものではなく Mac App Store のサンドボックスと非互換な点。「任意の他アプリにグローバルな入力を送る」動作はサンドボックスの外側なので、この種のアプリは App Store ではなく Developer ID + 公証で配布するのが定石になる。
- 仮に App Store に載せられる構成でも、permission プロンプトの前に理由を説明する(Guideline 2.5.1)・usage description を具体化する・secure input への配慮を怠ると "vague reject" を食らう。
- 入力注入 API 自体は各 OS で枯れているが、権限モデルは OS ごとに完全に別物(macOS=TCC / Windows=Integrity Level / Linux=uinput 権限)。横断アプリはここで非対称になる。
1. CGEvent とは
CGEvent は Core Graphics の Quartz Event Services が提供する低レベルイベント API。実デバイスを介さずにキーボード/マウスイベントをプログラムから合成し、OS のイベントストリームへ投入できる。ヘッダは <CoreGraphics/CGEventTypes.h> / <CoreGraphics/CGEventSource.h>。
CGEventCreateKeyboardEvent(source, keyCode, keyDown)— キー押下/解放CGEventCreateMouseEvent(source, type, position, mouseButton)— マウスCGEventCreateScrollWheelEvent(source, units, wheelCount, …)— スクロールCGEventPost(.cghidEventTap, event)— OS のイベントキューへ投入
イベントソース(CGEventSource)の状態で挙動が変わる:
kCGEventSourceStateHIDSystemState— 実デバイスと同等扱い。通常はこれ。kCGEventSourceStatePrivate— テスト用で UI には届かない。
キーコードは Carbon/HIToolbox/Events.h の kVK_ANSI_A 等で、キーボードレイアウト非依存の物理キー位置ベース。任意文字を直接入れたい場合は CGEventKeyboardSetUnicodeString(event, len, &chars) で キーコード不問の Unicode 文字注入ができる。日本語のような IME 経由の文字はこのパスを使うと、IME を介さず最終文字列をそのまま送り込める。
2. Accessibility(TCC)という関門
自プロセス内のイベント処理と違い、他アプリを外部から操作する目的で CGEvent を使うには Accessibility 権限が要る。ユーザーが「システム設定 → プライバシーとセキュリティ → アクセシビリティ」で明示的に許可する必要がある。プログラムからは AXIsProcessTrustedWithOptions() で許可状態の確認とプロンプト表示ができる。
未許可の状態で CGEventPost を呼んでも、API はエラーを返さず成功したかのように振る舞い、実際には何も起きない。例外もログも出ないため「コードは正しいのに動かない」状態になり、原因が権限だと気づくまで時間を溶かす。実装では起動時・操作前に AXIsProcessTrusted() で明示チェックし、未許可なら動線を出すのが必須。
TCC 権限には開発体験を悪くする固有事情がある。
- Accessibility 権限は bundle ID + コード署名チェーンにひもづく。
- 開発中にコード署名(証明書やチーム)が変わると権限が無効化され、再追加が必要になる。同じアプリに見えても OS 的には別物扱いされる。
- そのため入力系アプリは、環境設定に「TCC をリセットして再許可する」導線を持たせておくと開発・サポートが楽になる。
3. なぜ入力系アプリは審査・配布で詰まるのか
「CGEvent を使うと審査に落ちる」と一括りにされがちだが、原因は段階が違う 3 つに分かれる。順に潰していかないと理由の切り分けができない。
3.1. 根因: Mac App Store のサンドボックスと非互換
Mac App Store で配布するアプリは App Sandbox が必須。サンドボックスは「そのアプリが必要とするリソースだけに権限を絞る」仕組みで、任意の他アプリへグローバルに入力を送り込む動作は、この「絞る」思想の対極にある。Xcode でも Slack でもゲームでも、フォアグラウンドの任意のアプリを外から操作する——という CGEvent 注入の本質は、サンドボックスが許す範囲に収まらない。
- 結果として、グローバル入力注入を核にするユーティリティは Mac App Store には載せられず、Developer ID + 公証(notarization)で App Store 外配布するのが定石になる。
- Karabiner-Elements・BetterTouchTool・各種リモート操作/オートメーションアプリがいずれも App Store 外配布なのは、主にこの制約による。
- つまり「審査に通らない」の第一の答えは、そもそも App Store の配布モデルに乗せる前提が崩れているということ。落ちるのではなく、載せる土俵が違う。
3.2. App Store に載せる構成でも詰まる点: permission の説明不足
アプリを分割し、入力注入をしない側(例: iPhone アプリ本体)を App Store に出す構成にしても、権限プロンプトまわりの作法で落ちる。App Store Review Guideline 2.5.1 は「permission を要求する前に、なぜ必要かをユーザーに明示せよ」を求める。
- いきなり OS のプロンプトを出す実装(
requestAccess()を無説明で叩くだけ)は "vague"/唐突として弾かれやすい。 - 初回起動時などに onboarding シートで「何のために必要か / 許可後どう操作するか / 拒否しても他機能は使える旨」を説明し、ユーザーが「許可する」を押したときだけ OS プロンプト+システム設定を開くのが望ましい。UX 改善にもなる。
3.3. usage description が抽象的だと "vague reject"
ローカルネットワークや Bluetooth を使うリモート入力アプリでは、NSLocalNetworkUsageDescription / NSBluetoothAlwaysUsageDescription 等の 用途説明文が審査対象になる。「Mac との接続に使用します」程度の抽象的な文言は典型的な reject パターン。
✗ 「近くの Mac との接続にローカルネットワークを使用します。」
✓ 「Mac コンパニオンアプリへキーストロークやポインタ操作を送信するため、ローカルネットワーク (Bonjour) を使用します。広告・解析・端末識別には使用しません。」
- 何のデータが / どこへ / 何のために流れるかを書く。
- 広告・解析・トラッキングには使わないことを明記すると、レビュアーの疑念を先回りで潰せる。
- ローカライズ経路(ja/en)を用意する。ja のみで en 訳経路が無いのも指摘対象になりうる。
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. 実装上の含意
- 注入層は protocol で抽象化する。macOS は
CGEvent、Windows はSendInputを P/Invoke で薄くラップ——と、意味論レベルを揃えておくと横展開が対称的になる。 - ラッパーは 「文字入力モード(Unicode 注入)」と「キーコードモード(ショートカット/ゲーム)」を切り替えられる構造にしておく。将来のゲーム操作や AI computer use にも流用できる。
- 権限チェックを起動時・操作前に必ず挟む。silent fail を前提に、未許可なら onboarding/設定導線を出す。
- 配布戦略を最初に決める。グローバル入力注入が核なら App Store は諦め、Developer ID + 公証で配る前提で設計する。App Store に出すのは注入をしない側のアプリだけ、という分割も選択肢。
CGEvent は macOS の標準的な入力注入 API だが、他アプリを操作するには Accessibility 権限が必須で、未許可時は silent fail する。そして入力系アプリが審査で詰まる根因は権限ではなく Mac App Store サンドボックスとの非互換——多くは App Store 外配布が正解になる。App Store に載せる部分でも、permission 前の説明・usage description の具体化・secure input 配慮を外すと "vague reject" を招く。
7. 出典
- Quartz Event Services — Apple Developer Documentation
- CGEventCreateKeyboardEvent — Apple Developer Documentation
- CGEventKeyboardSetUnicodeString — Apple Developer Documentation
- AXIsProcessTrustedWithOptions — Apple Developer Documentation
- App Sandbox — Apple Developer Documentation
- App Store Review Guidelines(2.5.1 / 5.1 Privacy)— Apple
- NSLocalNetworkUsageDescription — Apple Developer Documentation
- Notarizing macOS software before distribution — Apple Developer Documentation
- SendInput function — Microsoft Learn(Windows 対応物)