Kolibriは小さなMac用ユーティリティです。fnキーとcontrolキーを押したままマウスを動かすと、ポインタの下のウインドウがついてきます。optionキーを加えると、最も近い角からリサイズします。Linuxのデスクトップにはずっと昔からあるAlt+ドラッグで、私はMacでこれがないのを毎日不便に感じていました。
こうしたユーティリティは、昔からアクセシビリティAPIを使って作られてきました。ポインタの下にあるウインドウをシステムに問い合わせ、マウスの動きに合わせてその位置を設定します。これはうまく動きます。ただし、Mac App Storeでは配布できません。アクセシビリティAPIはサンドボックスの中では使えず、App Storeのアプリはすべてサンドボックス化が必須だからです。Moom 4がMany Tricksの自社サイトでしか販売されていないのはこのためです。今もストアにあるMoomはMoom 3で、現在はMoom Classicという名前になっています。サンドボックスの規則ができる前にストアに入ったアプリで、新機能を追加しない限りはストアに残れます。
それでも私は、KolibriをApp Storeで出したいと考えました。多くの人はMacのアプリをそこで探しますし、自前の決済の仕組みを作りたくなかったからです。以下はその経緯です。
サンドボックス化されたアプリにできること
サンドボックス化されたアプリは、ほかのアプリのウインドウに手を出せません。ただし、適切なエンタイトルメントがあり、ユーザーが許可すれば、ほかのアプリにApple Eventsを送ることはできます。送れる相手のひとつがショートカットアプリで、ショートカットにはFind Windows、Move Window、Resize Windowというアクションがあります。これらはショートカットの中で、ショートカット自身の権限で動くので、私のアプリのサンドボックスの外で実行されます。
これは私が自分で見つけた方法ではありません。Mac App Storeにあるウインドウスナップアプリ、snAppが、すでにこれらのアクションを使うショートカットを同梱しています。私はそのショートカットファイルを読んで、形式を学びました。
そこで、App Store版のKolibriは次のように動きます。
- キーを押しているあいだ、Kolibriは独自の枠を描き、その枠をマウスに合わせて動かします。本物のウインドウはその場にとどまります。
- 手を離すと、KolibriはShortcuts EventsにApple Eventを1つ送ります。このイベントは「Kolibri Window Mover」というショートカットを、小さなJSONデータを渡して実行します。データの中身は、アプリの名前、そのアプリの何番目のウインドウか、そして新しいフレーム(位置とサイズ)です。
- ショートカットはデータを読み取り、Find Windowsでウインドウを見つけ、Move WindowとResize Windowで配置します。
このApple Eventは、AppleScriptで書くと1行です。
tell application "Shortcuts Events" to run shortcut "Kolibri Window Mover" with input "{\"app\":\"Safari\",\"i\":1,\"x\":0,\"y\":38,\"w\":1280,\"h\":771}"
サンドボックス以外に必要なエンタイトルメントは、Apple Eventsのエンタイトルメントと、Shortcuts Eventsに対するスクリプティングターゲットだけです。後者はショートカットの実行だけを許可し、それ以外は何もできません。ユーザーには、最初にウインドウを動かしたときにオートメーションの確認が1回表示されます。
ショートカット自体はリポジトリ内のPythonスクリプトで生成し、shortcuts sign --mode anyoneで署名しています。Macのショートカットは署名のないショートカットファイルを読み込まず、「anyone」モードなら誰でも読み込めます。署名済みのファイルはアプリに同梱しています。初回起動時にKolibriはshortcuts listを実行し、ショートカットが見つからなければそのファイルを開きます。するとショートカットがいつもの読み込み画面を表示します。ユーザーの操作はクリック1回です。
却下
最初に提出したビルドは、9月2日にApp Review(審査)で却下されました。理由は、アクセシビリティの使用がガイドライン2.4.5に、スクリーンショットが2.3.3に抵触するというものでした。
そのビルドでも、ウインドウの移動はすでにショートカット経由でした。却下の原因は、マウスの追跡とキーの読み取りの方法でした。CGEventタップを使っていて、これにはアクセシビリティの許可が必要です。そのためアプリがアクセシビリティの許可を求めることになり、アクセシビリティを求めるウインドウ操作ツールは、それを何に使うかに関係なく却下されます。
解決策は、それを必要としない作りにすることでした。App Store版は現在、標準のグローバルなマウス移動イベントモニタでポインタを追い、修飾キーはシステムの現在のフラグ状態から読み取っています。どちらもアクセシビリティや入力監視の許可は要りません。私はタップと、許可の確認処理と、許可を求めるメニュー項目を削除し、スクリーンショットをドラッグ途中の枠を写した無加工のキャプチャに差し替えました。2回目の提出で審査を通りました。
その代償
ウインドウが移るのは、手を離してから200〜330ミリ秒後です。これはApple Eventsとショートカットを往復する時間で、私にはどうにもできません。App Store版がドラッグ中にウインドウそのものではなく枠を表示するのはこのためです。マウスイベントのたびにショートカット経由で本物のウインドウを動かしていたら、遅すぎて使いものになりません。
また、ショートカットはウインドウをハンドルではなく、アプリ名とウインドウの順番で見つけます。実際には確実に動きますが、アクセシビリティで得られる情報と比べれば推測にすぎません。
さらにクリックモード、つまりキーを押したままクリックしてドラッグを始めるモードでは、App Store版はそのクリックを吸収できません。それにもイベントタップが必要だからです。クリックは下にあるウインドウに届いてしまいます。
2つのビルド
枠の表示はまずまずの妥協案ですが、私が作ろうとしていたアプリではありません。そこでビルドを2つにしました。
App Store版は、ここまで説明したものです。Kolibri Proはこのサイトから別にダウンロードするビルドで、Developer IDで署名し、公証済みで、サンドボックス化されていません。アクセシビリティを使い、元のKolibriと同じように、中身が見えたまま本物のウインドウをリアルタイムで動かします。ショートカットもオートメーションの確認も必要ありません。
ProはApp Store版を購入した人なら誰でも無料で使え、ライセンスキーもアカウントもサーバーもありません。起動時にProは、同じMacにあるApp Store版のKolibriを探し、そのレシートをオフラインで確認します。
- レシートの署名が、Appleのルート証明書までたどれること。この証明書は、システムの証明書ストアを信頼する代わりにPro自身が持っています。
- レシートに、App Store版アプリのバンドルIDが記載されていること。
- レシートのハッシュが、このMacのハードウェアIDと一致すること。そのため、別のMacから持ってきたコピーでは、App Storeからもう一度ダウンロードするまでロックが解除されません。
どれかに失敗すると、Proはその理由を表示し、App Storeを開くよう提案して終了します。支払いと返金はApp Storeが扱い、リアルタイムで動く版がほしい人もそれを手に入れられます。2つのビルドは1つのXcodeプロジェクトから作っています。ポインタの追跡、キー、設定、メニューバーは共通で、違うのはウインドウを動かす部分だけです。
ビルドを2つ出すというアイデアは、ATPでのJohn Siracusaの話を聞いて得たものです。
2つのビルドを並べた動画はプレスキットにあります。