[Mac]Windowのどこをドラッグしても移動できるアプリの実装について

# 作成したアプリの紹介

Macのウィンドウの任意の場所をCMDキーを押しながらドラッグして、ウィンドウの移動やリサイズを可能にするアプリ[DokodemoDrag](https://github.com/hmuronaka/DokodemoDrag)を作成しました。

![image01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37443/af6d8de3-e191-e5bd-7f15-5e71afbe4e90.gif)
設計・実装の記録も兼ねて、技術要素の知見を共有します。

※ DokodemoDragの実装は、[Rectangle](https://github.com/rxhanson/Rectangle)というキーボードショートカットでウィンドウ配置を行うアプリを参考にしています。

# 主な技術的要素

DokodemoDragが行っている事の概要は、

1. OSログイン時にアプリを自動起動し
2. アプリをMacOSに常駐させ
3. アプリ外のイベント(マウスイベント)を監視・取得し
4. そのイベント情報をもとに、DokodemoDragアプリ外のウィンドウの情報を取得・操作する

になります。 ここで紹介する技術要素は、上と順序が異なりますが

- マウス操作などのイベント監視
- アプリ外のウィンドウ操作
- 常駐アプリ化
- OS起動時の自動起動

です。次のそれぞれ解説します。

# 1. マウス操作などのイベント監視

アプリ外のイベント監視は、次のメソッドを利用することで実現できます。

[NSEvent.addGlobalMonitorForEvents(matching:handler:)](https://developer.apple.com/documentation/appkit/nsevent/1535472-addglobalmonitorforevents)

このメソッドは、アプリ外で発生したイベント(マウスイベントなど）を監視するためのメソッドです。

あくまでもイベントを監視するのみで、イベント内容を変更したり、イベントをキャンセル（元のアプリにイベントを発生させない）したりはできません。

キー関連のイベントを監視する場合は、macOSの「システム環境設定」「セキュリティとプライバシー」から「アクセシビリティ」が許可されている必要があります。

また監視できるイベントが限定されています。
（ドキュメントに監視可能なイベントの一覧が記載されています）

----

DokodemoDragでは、[MouseHookService.swift #24](https://github.com/hmuronaka/DokodemoDrag/blob/0.2/DokodemoDrag/MouseHookService.swift#L24)でイベント監視を開始するstartメソッドを定義し、アプリ起動時や機能を有効にした場合に監視を開始しています。([AppDelegate.swift #31](https://github.com/hmuronaka/DokodemoDrag/blob/0.2/DokodemoDrag/AppDelegate.swift#L31))

```swift
// MouseHookService.swift
    public func start() {
        stop()
        eventMonitor = 
            NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, 
                                                         .leftMouseDragged,
                                                         .leftMouseUp]) { event in
            self.handleMouseEvent(event)
        }
    }
    public func stop() {
        guard let eventMonitor = eventMonitor else { return }
        NSEvent.removeMonitor(eventMonitor)
        self.eventMonitor = nil
        self.element = nil
    }
```

----

# 2. アプリ外のウィンドウ操作

App Sandboxの無効化とアプリにAccessibility APIの利用を許可する必要があります。

## 2.1. App Sandboxの無効

アプリ外のウィンドウの操作を行うために、Xcodeプロジェクトにある
<プロジェクト名>.entitlementsファイルの項目App SandboxをNOにする
にする必要があります。

[App Sandbox](https://developer.apple.com/documentation/security/app_sandbox)は、ドキュメントに

> 「macOSアプリケーションのシステムリソースとユーザーデータへのアクセスを制限し、アプリケーションが侵害された場合の被害を抑制します。」

とあります。(DeepL翻訳)

アプリ外のウィンドウの位置変更やリサイズはAccessibilityのAPIを利用する必要があり、このAPIを利用するためにApp Sandboxを無効にする必要があります。

但し**App Sandboxを無効にすると、そのアプリはMac App Storeから配信することはできなくなります。**

App Sandboxを無効にした場合、Mac App Storeから配信することはできませんが、[Developer ID](https://developer.apple.com/jp/support/developer-id/)を利用することでAppleの公証（Notarization)を受けることができ、macOSで安全にアプリを開くことができるようになります。[参考: Mac でAppを安全に開く](https://support.apple.com/ja-jp/HT202491)

---- 
Accessibility APIを利用しているにも関わらず、一部のアプリ(Cinch, BetterSnapTool, PopClipなど)はStoreに配信されているようですが、これらはstack overflowのによると、Sandboxがリリースされる以前に公開されたアプリのようで特例のようです。

[stackoverflow: How to use Accessibility with sandboxed app?](https://stackoverflow.com/questions/32116095/how-to-use-accessibility-with-sandboxed-app)
(これらのアプリが、2021年現在もAccessibility APIの機能が利用可能なのかは把握していません）

----

DokodemoDragでは、Rectangleで定義されている[AccessibilityElementクラス](https://github.com/hmuronaka/DokodemoDrag/blob/0.2/DokodemoDrag/AccessibilityElement.swift#L34)を利用して、Accessibility APIを利用しています。
AccessibilityElementはElement単位での操作が用意されているので、私のようにAccessibility APIの詳細を把握できていなくても直感的に利用できるようになっています。

実際のウィンドウ移動/リサイズ箇所は[MouseHookService.swift #49, #51](https://github.com/hmuronaka/DokodemoDrag/blob/main/DokodemoDrag/MouseHookService.swift#L49)
になります。

----

## 2.2 アクセシビリティの許可

アプリがAccessibility APIを利用するために、 このアプリをmacOSの「システム環境設定」「セキュリティとプライバシー」から「アクセシビリティ」の許可する必要があります。

# 3. 常駐アプリ化

LSUIElementの設定とステータスバーへのメニューの配置を行います。

## 3.1 LSUIElement

Dockにも現れず、強制終了一覧にも表示されないアプリを作成するには、Info.plistで[LSUIElement](https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html#//apple_ref/doc/uid/20001431-108256)をYESにします。

## 3.2 ステータスバーへのメニュー配置

ステータスバーへのメニュー配置は、Rectangleの実装をベースとしています。[StatusBarItem #56](https://github.com/hmuronaka/DokodemoDrag/blob/0.2/DokodemoDrag/StatusBarItem.swift#L56)

また私自身Macアプリ開発については素人同然なので、常駐化の把握については「[ステータスバー常駐アプリ](http://saokkk.seesaa.net/article/443713479.html)」という記事を参考にしました。

# 4. OSログイン時のアプリ自動起動

MacでのOSログイン時の複数あるようで、実装の際には「 [「ログイン時に起動」を実装する](https://questbeat.hatenablog.jp/entry/2014/04/19/123207)」と[Rectangle](https://github.com/rxhanson/Rectangle)の実装を参考にしました。

記事には、自動起動の選択肢として

- [Launch Services](https://developer.apple.com/documentation/coreservices/launch_services) を使う方法と
- [Helper Application](https://developer.apple.com/documentation/servicemanagement/1501557-smloginitemsetenabled?language=objc) を使う方法

などが紹介されています。

Launch Serviceを利用する場合、OSのログイン項目に登録する方法となっていて、システム環境設定の「ユーザとグループ」の「ログイン項目」に表示されます。

一方Helper Applicationは、主アプリのmain bundleのContents/Library/LoginItems/内にあるヘルパーアプリが、主アプリを起動する仕組みとなっています。

自動起動の注意点としては、**自動ログインは、Mac App Storeのガイドラインにより、「ユーザへの確認なしの自動起動はしてはならない」ため、アプリ起動時や設定で、ユーザが自動起動を有効にする必要があります。**

[Mac App Store Guidelines](https://developer.apple.com/app-store/review/guidelines/)（以下は原文まま)
> (iii) They may not auto-launch or have other code run automatically at startup or login without consent nor spawn processes that continue to run without consent after a user has quit the app. They should not automatically add their icons to the Dock or leave shortcuts on the user desktop.

----

DokodemoDragでは、Rectangleの実装を参考にしてHelper Applicationによる実装となっています。

Helper Applicationによる実現方法の概要は

- 主アプリを起動するHelper Application(ここではDokodemoDragLauncher)を作成する。
  - Helper Applicationは、主アプリを起動する。([AppDelegate.swift #35](https://github.com/hmuronaka/DokodemoDrag/blob/0.2/DokodemoDragLauncher/DokodemoDragLauncher/AppDelegate.swift#L35))
    ※ 現状のコードでは警告が出ます。またこのアプリの起動方法はSandboxを無効にしている場合のみ動作します。
  - Helper ApplicationのInfo.plistの「Application is agent(UIElement)か「Application is background only」を有効にする
  - Build Settingsでskip installをYESにする。（これを行わないと、主アプリをOrganizerからDeveloper-idでDitributeできません）
- 主アプリ側
  - Build PhaseにCopy Files Build Phaseを追加し、Contents／Library/LoginItems/にヘルパーアプリケーションを追加する
  - [SMLoginItemSetEnabled](https://developer.apple.com/documentation/servicemanagement/1501557-smloginitemsetenabled?language=objc)を呼び出す。([SettingService.swift #36](https://github.com/hmuronaka/DokodemoDrag/blob/0.2/DokodemoDrag/SettingService.swift#L36))

です。 

----

つらつらHelpr Applicationについて書きましたが、自動起動についてはHerlp Applicationをライブラリ化されている[sindresorhus/LaunchAtLogin](https://github.com/sindresorhus/LaunchAtLogin)を利用をすると、楽ができるかもしれません。

----

記録も兼ねて長々書きましたが、常駐アプリを作成する際の参考になれば幸いです。

良ければ[DokodemoDrag](https://github.com/hmuronaka/DokodemoDrag)も触ってみてください。

以上!!