SwiftUI で MacOS メニューバーアプリケーションを手軽に開発

SwiftUI を使えば、メニューバーアプリケーションを簡単に開発できます。 

メニューバーアプリケーションを設置することで、最上部システムバーにアプリケーションが表示され、ユーザーはこのアイコンをクリックしてアプリケーションのポップオーバー表示にすばやくアクセスできるようになります。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/caf5f224-0682-50a8-7839-0c3f64dc6611.png">

アプリレビューを待つ間に、システムバー内でタスクリストを管理できる小さな TODO アプリケーションを最近開発しました。ソースコードをダウンロードして仕組みを確認することができます。
https://github.com/mszmagic/MenuBarTODO
https://apps.apple.com/jp/app/menubartodo/id1550343840?mt=12

## MacOSアプリケーションの作成

まず、SwiftUIを有効にした空のMacOSアプリケーションを作成します。アプリケーションの種類として `AppKit App Delegate` を選択していることを確認してください。

![スクリーンショット 0003-01-22 15.35.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/1e19e33a-220f-00e4-f159-5f9473276e55.png)

## `AppDelegate.swift` ファイルの設定

メニューバーアイテムとポップオーバーウィンドウをこのファイルで設定する必要があります。この記事では、`ContentView()` をポップアップウィンドウとして使用します。

アプリケーションで一般的なインターフェースも使用したい場合には、ポップアップウィンドウ専用の個別のSwiftUIビューを作成する必要があります。

### 変数の設定

以下の変数を `AppDelegate.swift` ファイルに追加：

```swift
var statusBarItem: NSStatusItem!
var popover: NSPopover!
```

### ステータスバーのアイテムを初期化する

`statusBarItem` は、システムのメニューバーに表示される項目です。このオブジェクトをイメージ（またはシステムイメージ）で初期化する必要があります。

```swift
self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
guard let button = self.statusBarItem.button else { return }
button.image = NSImage(systemSymbolName: "checkmark.circle.fill", accessibilityDescription: nil)
button.action = #selector(showHidePopover(_:))
```

この記事の次の部分で `showHidePopover` 関数を定義します。

### ポップオーバーを初期化する

ここで、ポップオーバーウィンドウを初期化して、そのウィンドウのコンテンツとなるSwiftUIのビューを設定する必要があります。

```swift
let popover = NSPopover()
popover.contentSize = NSSize(width: 350, height: 500)
popover.behavior = .transient
let contentView = ContentView().environment(\.managedObjectContext, persistenceController.storageContext)
popover.contentViewController = NSHostingController(rootView: contentView)
self.popover = popover
```

### ユーザーがメニュー項目をクリックした際にポップオーバーを表示または非表示

また、ポップオーバーを表示または非表示にする機能 `showHidePopover` も定義する必要があります。

```swift
@objc func showHidePopover(_ sender: AnyObject?) {
    guard let button = self.statusBarItem.button else { return }
    if self.popover.isShown {
        self.popover.performClose(sender)
    } else {
        self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
        self.popover.contentViewController?.view.window?.becomeKey()
    }
}
```

### プログラムを実行する

これでプログラムが実行できます。システムメニューバーに新しいメニュー項目が表示されるはずです。

## システムメニューバーにしか表示されないアプリケーションの作成

Xcodeでプロジェクトを作成すると、XcodeではSwiftUIのビューがウィンドウに表示されます。

```swift
func applicationDidFinishLaunching(_ aNotification: Notification) {
    // Create the SwiftUI view that provides the window contents.
    let contentView = ContentView()

    // Create the window and set the content view.
    window = NSWindow(
        contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
        styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
        backing: .buffered, defer: false)
    window.isReleasedWhenClosed = false
    window.center()
    window.setFrameAutosaveName("Main Window")
    window.contentView = NSHostingView(rootView: contentView)
    window.makeKeyAndOrderFront(nil)
}
```

`window` 変数を削除し、`applicationDidFinishLaunching` 関数内のすべての関連コードを削除する（メニュー項目とポップオーバーを初期化するためのコードは保持します）ことで、アプリケーションをシステムメニューバーでのみ表示するようにできます。

```swift
func applicationDidFinishLaunching(_ aNotification: Notification) {
    let contentView = ContentView()
    // ...
}
```

また、システムがアプリケーションウィンドウの表示を試みないようにするには、`Info.plist`ファイル内の`LSUIElement`を`YES`に設定する必要があります。

![スクリーンショット 0003-01-22 14.39.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/1a60153c-1783-6d9b-be7b-75d182c9e102.png)

ところでそのアプリケーションは、システムメニューバーにのみ表示され、アプリケーションウィンドウは表示しません。

Githubのこちらのデモプロジェクトをご覧ください。上記のコードが含まれています。
https://github.com/mszmagic/MenuBarTODO

---
:relaxed: [Twitter @MszPro](https://twitter.com/MszPro)

:sunny: [私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。](http://mszpro.com/ioskiji/)
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">